Unity’de Yüksek Performanslı Veri Yönetimi: NativeList

Unity'de NativeList ile veri yönetimini öğrenin. Performanslı ve çok çekirdekli sistemlere uyumlu oyun geliştirmek için NativeList'in temellerini, kullanımını ve ipuçlarını keşfedin.

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`, esneklik sunsa da, Unity’nin İşler (Jobs) sistemi ve Burst derleyicisi gibi yüksek performanslı araçlarıyla tam uyumlu değildir. İşte tam bu noktada Unity NativeList devreye girer. `NativeList`, Unity’nin Data-Oriented Technology Stack (DOTS) yaklaşımının bir parçası olarak, yönetilmeyen (unmanaged) bellekte dinamik boyutlu veri koleksiyonları oluşturmanızı sağlar. Bu, özellikle çok çekirdekli işlemcilerden tam verim almak ve ana iş parçacığının (main thread) yükünü hafifletmek için hayati öneme sahiptir.

Bu makalede, `NativeList`’nin temellerini, geleneksel koleksiyonlardan farklarını, nasıl kullanılacağını, yaygın hataları ve performans ipuçlarını detaylı bir şekilde inceleyeceğiz. Amacımız, Unity projelerinizde daha optimize edilmiş ve yüksek performanslı veri yapıları kullanmanıza yardımcı olmaktır.

NativeList Nedir ve Nasıl Çalışır?

`NativeList`, Unity’nin düşük seviyeli bellek yönetimi API’leri üzerine inşa edilmiş, dinamik boyutlu bir koleksiyon türüdür. `List`’ye benzer şekilde eleman ekleyebilir, silebilir ve erişebilirsiniz, ancak temel farkı belleği yönetilen yığın (managed heap) yerine yönetilmeyen bellekte tahsis etmesidir. Bu sayede, C# çöp toplayıcısından (Garbage Collector – GC) bağımsız çalışır ve performans kritik senaryolarda takılmaları (stuttering) önler.

Geleneksel List ile Farkları

En önemli fark, `NativeList`’nin yönetilmeyen bellekte yer almasıdır. `List` ise yönetilen bellekte nesneleri saklar ve bu nesnelerin ömrünü çöp toplayıcı yönetir. Çöp toplayıcı, çalışma zamanında performansa olumsuz etki edebilecek duraklamalara neden olabilir. `NativeList` ile bellek yönetimi tamamen sizin sorumluluğunuzdadadır; bu da daha fazla kontrol ve deterministik performans anlamına gelir. Ayrıca, `NativeList`, Unity Jobs sistemi ve Burst derleyicisi ile sorunsuz bir şekilde entegre olacak şekilde tasarlanmıştır. Bu araçlar, yönetilmeyen bellekteki verilere doğrudan ve güvenli bir şekilde erişerek paralel işlem yeteneklerini en üst düzeye çıkarır.

Bellek Yönetimi ve Allocator

`NativeList` oluştururken, belleğin nasıl tahsis edileceğini belirten bir `Allocator` türü belirtmeniz gerekir. Üç ana `Allocator` tipi bulunur:

  • `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

`NativeList`, `List` gibi bir kapasiteye (Capacity) 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`’deki veriler, bellek üzerinde bitişik (contiguous) bir blokta saklanır. Bu, CPU önbelleği (cache) açısından son derece verimlidir. Bitişik belleğe erişim, CPU’nun bir sonraki ihtiyacı olan veriyi tahmin etmesini ve önbelleğe almasını kolaylaştırır, bu da bellek okuma/yazma işlemlerinin hızını artırır. Geleneksel `List`’deki referans tiplerin dağınık bellek dağılımı, önbellek isabet oranını düşürerek performansı olumsuz etkileyebilir.

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.

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir