Giriş: Neden NativeList?
Unity geliştiricileri olarak, özellikle büyük ölçekli ve performans odaklı oyunlar geliştirirken veri yönetimi kritik bir rol oynar. Geleneksel C# koleksiyonları, örneğin `List
Bu makalede, `NativeList
NativeList Nedir ve Nasıl Çalışır?
`NativeList
Geleneksel List ile Farkları
En önemli fark, `NativeList
Bellek Yönetimi ve Allocator
`NativeList
- `Allocator.Temp`: En kısa ömürlü bellek tahsisi için kullanılır. Genellikle tek bir kare (frame) veya çok kısa süreli işlemler için idealdir. Performansı en yüksektir ancak dikkatli kullanılmazsa hızlıca bellek sızıntılarına yol açabilir. Genellikle `Job` içinde geçici koleksiyonlar için kullanılır.
- `Allocator.TempJob`: Birkaç kare veya bir `Job`’ın ömrü boyunca kullanılacak bellek tahsisleri içindir. `Allocator.Temp`’e göre biraz daha uzun ömürlüdür ve `Job` sistemi tarafından otomatik olarak güvenli bir şekilde takip edilir.
- `Allocator.Persistent`: Uygulamanın ömrü boyunca veya belirgin bir bölümünde kalacak uzun ömürlü bellek tahsisleri için kullanılır. Bu tip bellek tahsisleri manuel olarak `Dispose()` edilmelidir. Genellikle bir `MonoBehaviour`’ın `OnDestroy` metodunda veya belirli bir yaşam döngüsü sonunda serbest bırakılır.
Temel Kullanım
Bir Unity NativeList oluşturmak ve kullanmak oldukça basittir. Ancak, bellek yönetimi sorumluluğunu unutmamak gerekir.
using Unity.Collections;
using UnityEngine;
public class NativeListExample : MonoBehaviour
{
private NativeList<int> myNativeList;
void Start()
{
// Persistent allocator ile bir NativeList oluşturun
myNativeList = new NativeList<int>(10, Allocator.Persistent);
// Eleman ekleme
myNativeList.Add(5);
myNativeList.Add(10);
myNativeList.Add(15);
// Elemanlara erişim
Debug.Log("İlk eleman: " + myNativeList[0]);
// Eleman silme
myNativeList.RemoveAt(0);
Debug.Log("Yeni ilk eleman: " + myNativeList[0]); // Şimdi 10 olacak
// NativeList'in boyutunu kontrol etme
Debug.Log("Liste boyutu: " + myNativeList.Length);
}
void OnDestroy()
{
// Bellek sızıntısını önlemek için NativeList'i dispose edin
if (myNativeList.IsCreated)
{
myNativeList.Dispose();
}
}
}
Yukarıdaki örnekte görebileceğiniz gibi, `NativeList` bir `IDisposable` arayüzünü uygular. Bu, tahsis edilen yönetilmeyen belleğin manuel olarak serbest bırakılması gerektiği anlamına gelir. `Dispose()` metodunu çağırmayı unutmak, bellek sızıntılarına yol açar ve uygulamanızın performansını ve kararlılığını olumsuz etkiler.
Unity NativeList ile Pratik İpuçları
1. Unity Jobs Sistemi ile Entegrasyon
Unity NativeList‘in en güçlü yönlerinden biri, Unity Jobs sistemi ile sorunsuz entegrasyonudur. `NativeList`, `IJob` veya `IJobParallelFor` arayüzlerini uygulayan işlerde güvenli bir şekilde kullanılabilir. İşler genellikle `NativeArray` kullanır, ancak dinamik boyutlu veri gerektiren durumlarda `NativeList` tercih edilir. İş içinde bir `NativeList`’e yazarken veya okurken, güvenlik denetimleri nedeniyle performans düşüşünü önlemek için `[WriteOnly]` veya `[ReadOnly]` niteliklerini kullanmayı düşünebilirsiniz.
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
public struct MyJob : IJob
{
public NativeList<int> results;
public void Execute()
{
for (int i = 0; i < 10; i++)
{
results.Add(i * 2);
}
}
}
public class JobNativeListExample : MonoBehaviour
{
void Start()
{
// TempJob allocator, iş bittiğinde otomatik olarak dispose edilir
NativeList<int> jobResults = new NativeList<int>(0, Allocator.TempJob);
MyJob job = new MyJob { results = jobResults };
JobHandle handle = job.Schedule();
// İşin bitmesini bekleyin
handle.Complete();
foreach (var item in jobResults)
{
Debug.Log("Job Sonucu: " + item);
}
// TempJob, handle.Complete() çağrıldığında otomatik olarak dispose edilir,
// ancak manuel Dispose etmek de güvenlidir.
if (jobResults.IsCreated)
{
jobResults.Dispose();
}
}
}
2. Ne Zaman NativeArray, Ne Zaman NativeList?
Bu, sıkça sorulan bir sorudur. Karar, veri yapınızın dinamik olup olmadığına bağlıdır:
- `NativeArray
`: Boyutu sabit olan veri koleksiyonları için idealdir. Performans açısından genellikle `NativeList `’den daha hafiftir, çünkü yeniden boyutlandırma maliyeti yoktur. Eğer bir işin sonucunda belirli sayıda eleman bekliyorsanız veya verinin boyutu önceden biliniyorsa `NativeArray` kullanın. - `NativeList
`: Çalışma zamanında eleman eklemeniz veya silmeniz gerektiğinde, yani dinamik boyutlu bir koleksiyona ihtiyacınız olduğunda tercih edilir. `NativeList `, `NativeArray `’nin dinamik bir versiyonu olarak düşünülebilir.
3. Doğru Dispose() Stratejileri
`Dispose()` metodunu çağırmak, bellek sızıntılarını önlemenin anahtarıdır. İşte bazı stratejiler:
- `using` Bloğu: Kısa ömürlü `NativeList`’ler için `using` bloğu kullanmak, `Dispose()`’un otomatik olarak çağrılmasını sağlar. Ancak, bu genellikle `Allocator.Temp` veya `Allocator.TempJob` ile kullanılır ve `MonoBehaviour`’larda doğrudan kullanılamaz.
- `MonoBehaviour.OnDestroy()`: Bir `MonoBehaviour`’ın ömrü boyunca kullanılan `NativeList`’ler için en yaygın ve güvenli yöntemdir. Nesne yok edildiğinde `Dispose()` çağrılır.
- Manuel Yönetim: Bazı durumlarda, bir `NativeList`’in ömrü bir `MonoBehaviour`’a bağlı olmayabilir. Bu gibi durumlarda, koleksiyonun kullanım ömrü sona erdiğinde `Dispose()`’u çağırmak tamamen sizin sorumluluğunuzdadır.
4. Kapasite Yönetimi
`NativeListCapacity) sahiptir. Eleman ekledikçe ve kapasite doldukça, `NativeList` otomatik olarak daha büyük bir bellek bloğu tahsis eder ve mevcut elemanları yeni bloğa kopyalar. Bu yeniden tahsis (reallocation) işlemi performans maliyeti yaratır. Mümkünse, `NativeList`’i başlangıçta beklenen maksimum boyuta yakın bir kapasiteyle başlatmak, gereksiz yeniden tahsisleri önler ve performansı artırır.
// Yaklaşık 100 eleman tutacak bir NativeList oluşturun
NativeList<MyStruct> myOptimizedList = new NativeList<MyStruct>(100, Allocator.Persistent);
Ayrıca, `TrimExcess()` metodunu kullanarak, listenin mevcut eleman sayısına göre kapasitesini azaltabilirsiniz. Bu, eğer çok fazla eleman sildiyseniz ve belleği geri kazanmak istiyorsanız faydalı olabilir.
Yaygın Hatalar ve Çözümleri
Dispose() Unutmak
Bu, Unity NativeList kullanırken yapılan en yaygın hatadır. `Dispose()`’u çağırmamak, yönetilmeyen bellek sızıntılarına yol açar. Uygulamanız zamanla daha fazla bellek tüketir ve en sonunda çöker. Çözüm, her zaman `NativeList`’in ömrünü takip etmek ve işi bittiğinde `Dispose()` metodunu çağırmaktır. `MonoBehaviour`’lar için `OnDestroy` iyi bir yerdir.
Dispose() Edilmiş Bir NativeList’e Erişmek
Bir `NativeList` `Dispose()` edildikten sonra, ona erişmeye çalışmak bir çalışma zamanı hatasına (`InvalidOperationException`) neden olur. Bu hatayı önlemek için, erişmeden önce `IsCreated` özelliğini kontrol edin:
if (myNativeList.IsCreated)
{
// Güvenli bir şekilde erişebiliriz
Debug.Log(myNativeList[0]);
}
Güvenlik Kısıtlamaları ve [NativeDisableContainerSafetyRestriction]
Unity Jobs sistemi, veri yarışlarını (data races) önlemek için sıkı güvenlik denetimleri uygular. Örneğin, aynı `NativeList`’e farklı işlerden aynı anda yazmaya çalışmak bir hataya neden olur. Nadir durumlarda, bu güvenlik kısıtlamalarını bilerek atlamak isteyebilirsiniz (örneğin, okuma/yazma erişimini garanti ettiğiniz çok özel senaryolarda). Bu durumda `[NativeDisableContainerSafetyRestriction]` niteliğini kullanabilirsiniz. Ancak, bu niteliği kullanmak son derece risklidir ve sadece ne yaptığınızdan eminseniz kullanılmalıdır, aksi takdirde veri bozulmasına veya kararlılık sorunlarına yol açabilir.
Performans ve Optimizasyon Notları
Burst Derleyici Entegrasyonu
Unity NativeList, Burst derleyicisi ile birlikte kullanıldığında tam potansiyeline ulaşır. Burst, C# kodunu yüksek performanslı makine koduna dönüştürerek, CPU’nun SIMD (Single Instruction, Multiple Data) yeteneklerinden faydalanır. `NativeList`’in yönetilmeyen bellekte olması, Burst’ün verilere doğrudan erişmesini ve optimize etmesini kolaylaştırır, bu da inanılmaz hız artışları sağlar.
Önbellek Dostu Bellek Erişimi
`NativeList
Yeniden Tahsislerden Kaçınma
Yukarıda bahsedildiği gibi, `NativeList`’in kapasitesi dolduğunda yeniden tahsis yapması pahalı bir işlemdir. Mümkünse, başlangıç kapasitesini iyi tahmin edin veya `Clear()` metodunu kullanarak listeyi boşaltın ancak tahsis edilen belleği koruyun. Eğer bir `NativeList`’i sık sık tamamen boşaltıp tekrar dolduruyorsanız, her seferinde `new` ile oluşturmak yerine mevcut listeyi `Clear()` ile temizleyip yeniden kullanmak daha verimlidir.
Sonuç
Unity NativeList, Unity’de yüksek performanslı ve çok çekirdekli sistemlere uyumlu uygulamalar geliştirmek için güçlü bir araçtır. Yönetilmeyen bellekte dinamik veri koleksiyonları oluşturma yeteneği, çöp toplayıcıdan kaynaklanan duraklamaları ortadan kaldırır ve Unity Jobs ile Burst derleyicisinin tüm gücünden faydalanmanızı sağlar. Ancak, bu güçle birlikte bellek yönetimi sorumluluğu da gelir. `Dispose()` metodunu düzenli olarak çağırmayı, kapasiteyi optimize etmeyi ve Jobs sistemiyle entegrasyon kurallarını anlamayı unutmayın. Bu teknikleri ustaca kullanarak, Unity projelerinizde önemli performans artışları elde edebilir ve daha akıcı oyun deneyimleri sunabilirsiniz.



