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

Unity'de NativeHashMap kullanarak yüksek performanslı, iş parçacığı güvenli veri yapıları oluşturun. Bellek yönetimi, Jobs sistemi entegrasyonu ve optimizasyon ipuçları.

Unity projelerinde performans kritik öneme sahiptir ve veri yönetimi bu performansın temel taşlarından biridir. Özellikle çok çekirdekli işlemcilerden tam verim almak, yani verileri paralel olarak işlemek istediğimizde, standart C# koleksiyonları bazı kısıtlamalar getirebilir. İşte bu noktada Unity’nin NativeHashMap koleksiyonu devreye giriyor. Bu makalede, NativeHashMap‘in ne olduğunu, neden kullanmamız gerektiğini, temel kullanımını, pratik ipuçlarını, yaygın hataları ve performans optimizasyonlarını detaylı bir şekilde inceleyeceğiz.

NativeHashMap Nedir ve Neden Kullanmalıyız?

NativeHashMap<TKey, TValue>, Unity’nin Unity.Collections namespace’inde yer alan, Jobs sistemi ve Burst derleyicisi ile uyumlu, yüksek performanslı bir hash map (sözlük) veri yapısıdır. Standart C# Dictionary<TKey, TValue>‘dan farklı olarak, NativeHashMap managed (yönetilen) bellek yerine unmanaged (yönetilmeyen) bellek üzerinde çalışır. Bu sayede Garbage Collector (Çöp Toplayıcı) yükünden kurtulur ve özellikle paralel iş parçacıklarında (Jobs) güvenli ve hızlı veri erişimi sağlar.

Temel Farklar: NativeHashMap vs. Dictionary<TKey, TValue>

  • Bellek Yönetimi: Dictionary, .NET’in yönetilen belleğini kullanırken, NativeHashMap doğrudan işletim sisteminden bellek tahsis eder. Bu, GC (Garbage Collector) duraklamalarını ortadan kaldırır.
  • Yapı Tipi (Struct vs. Class): Dictionary bir referans tipi (class) iken, NativeHashMap bir değer tipidir (struct). Bu, özellikle Jobs sistemi içinde veri kopyalama ve erişim şekillerini etkiler.
  • İş Parçacığı Güvenliği: Dictionary varsayılan olarak iş parçacığı güvenli değildir ve birden fazla iş parçacığından aynı anda erişim hatalara yol açabilir. NativeHashMap ise Jobs sistemi ile kullanıldığında iş parçacığı güvenliği sağlayacak şekilde tasarlanmıştır (örneğin, [ReadOnly] veya [WriteOnly] attribute’ları ile).
  • Performans: Burst derleyici ve Jobs sistemi ile birleştiğinde, NativeHashMap, Dictionary‘ye göre önemli ölçüde daha iyi performans sunabilir, özellikle büyük veri setleri ve paralel işlemler söz konusu olduğunda.

NativeHashMap Temel Kullanımı

NativeHashMap kullanırken dikkat etmeniz gereken en önemli nokta bellek yönetimidir. Unmanaged bellek kullandığı için, tahsis ettiğiniz belleği işiniz bittiğinde manuel olarak serbest bırakmanız (Dispose() etmeniz) gerekir.

Başlatma ve Veri Ekleme

Bir NativeHashMap oluştururken başlangıç kapasitesi ve bir Allocator tipi belirtmeniz gerekir.

using Unity.Collections;
using UnityEngine;

public class NativeHashMapExample : MonoBehaviour
{
    private NativeHashMap<int, string> myHashMap;

    void Start()
    {
        // 16 başlangıç kapasitesi ve kalıcı bellek tahsisi ile oluştur
        myHashMap = new NativeHashMap<int, string>(16, Allocator.Persistent);

        // Veri ekleme
        myHashMap.Add(1, "Elma");
        myHashMap.Add(2, "Armut");
        myHashMap.Add(3, "Çilek");

        Debug.Log($"HashMap'te {myHashMap.Count} eleman var.");

        // Bir anahtarın olup olmadığını kontrol etme
        if (myHashMap.ContainsKey(2))
        {
            Debug.Log("2 numaralı anahtar mevcut.");
        }

        // Değer alma
        if (myHashMap.TryGetValue(1, out string value))
        {
            Debug.Log($"1 numaralı anahtarın değeri: {value}");
        }

        // Var olan bir anahtara yeni değer atama (Replace işlevi görür)
        myHashMap[1] = "Yeni Elma";
        Debug.Log($"1 numaralı anahtarın yeni değeri: {myHashMap[1]}");
    }

    void OnDestroy()
    {
        // Belleği serbest bırakmayı unutmayın!
        if (myHashMap.IsCreated)
        {
            myHashMap.Dispose();
            Debug.Log("NativeHashMap belleği serbest bırakıldı.");
        }
    }
}

Allocator tipleri şunlardır:

  • Allocator.Temp: Çok kısa ömürlü tahsisler için. Aynı kare içinde veya bir sonraki karede Dispose edilmelidir. En hızlı tahsis.
  • Allocator.TempJob: Bir Job’un ömrü boyunca kullanılacak tahsisler için. Job tamamlandığında Dispose edilmelidir.
  • Allocator.Persistent: Uygulamanın ömrü boyunca veya daha uzun süreli tahsisler için. Manuel olarak Dispose edilmelidir. En yavaş tahsis.

Pratik İpuçları

1. Jobs Sistemi ile Kullanım

NativeHashMap‘in asıl gücü Jobs sistemi ile birleştiğinde ortaya çıkar. Bir Job içinde NativeHashMap‘i kullanırken, Job struct’ına NativeHashMap‘i eklemeniz ve erişim tipini belirtmeniz gerekir.

using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public struct MyJob : IJobParallelFor
{
    [ReadOnly] public NativeArray<int> input;
    [WriteOnly] public NativeHashMap<int, int>.ParallelWriter outputMap;

    public void Execute(int index)
    {
        int key = input[index];
        int value = key * 2;
        outputMap.TryAdd(key, value);
    }
}

public class NativeHashMapJobExample : MonoBehaviour
{
    private NativeArray<int> _inputArray;
    private NativeHashMap<int, int> _resultMap;
    private JobHandle _jobHandle;

    void Start()
    {
        _inputArray = new NativeArray<int>(100, Allocator.TempJob);
        for (int i = 0; i < _inputArray.Length; i++)
        {
            _inputArray[i] = i;
        }

        _resultMap = new NativeHashMap<int, int>(_inputArray.Length, Allocator.TempJob);

        MyJob job = new MyJob
        {
            input = _inputArray,
            outputMap = _resultMap.AsParallelWriter()
        };

        _jobHandle = job.Schedule(_inputArray.Length, 32);
    }

    void Update()
    {
        if (_jobHandle.IsCompleted)
        {
            _jobHandle.Complete(); // Job'un tamamlanmasını bekle

            foreach (var kvp in _resultMap)
            {
                // Debug.Log ile doğrudan kullanmak yerine veriyi başka bir yere aktarılabilir
                // Debug.Log($"Anahtar: {kvp.Key}, Değer: {kvp.Value}");
            }
            Debug.Log($"Job tamamlandı. Sonuç HashMap'inde {_resultMap.Count} eleman var.");

            _inputArray.Dispose();
            _resultMap.Dispose();
            enabled = false; // Script'i devre dışı bırak
        }
    }
}

AsParallelWriter() metodu, birden fazla iş parçacığının aynı NativeHashMap‘e güvenli bir şekilde yazmasını sağlar.

2. Kapasite Planlaması

NativeHashMap‘i oluştururken doğru başlangıç kapasitesini belirlemek, performans açısından kritik öneme sahiptir. Eğer harita dolarsa, Unity otomatik olarak daha büyük bir harita tahsis edip mevcut verileri yeni haritaya kopyalar. Bu işlem pahalı olabilir. Mümkün olduğunca, haritanızda depolayacağınız maksimum eleman sayısını tahmin edip başlangıç kapasitesini buna göre ayarlayın.

3. `Dispose` ve `IsCreated` Kontrolü

NativeHashMap bir struct olduğu için, onu bir MonoBehaviour’dan başka bir yere kopyaladığınızda, aslında bellek referansını değil, struct’ın kendisini kopyalamış olursunuz. Bu durum, yanlışlıkla birden fazla kez Dispose etme veya zaten Dispose edilmiş bir haritaya erişme sorunlarına yol açabilir. Her zaman myHashMap.IsCreated kontrolünü kullanarak haritanın hala geçerli olup olmadığını kontrol edin.

Yaygın Hatalar ve Çözümleri

1. `Dispose()` Etmeyi Unutmak

Hata: NativeHashMap veya diğer Native koleksiyonları kullanıp, işiniz bittiğinde Dispose() etmemek. Bu durum bellek sızıntılarına yol açar ve Unity’de “A Native Collection has not been disposed, resulting in a memory leak” uyarısını almanıza neden olur.

Çözüm: Bir MonoBehaviour içinde kullanıyorsanız, OnDestroy() metodunda Dispose() çağrısını yapın. Bir Job içinde kullanıyorsanız, Job tamamlandığında (jobHandle.Complete() sonrası) Dispose() edin. Her zaman if (myHashMap.IsCreated) kontrolünü ekleyin.

2. Dispose Edilmiş Haritaya Erişim

Hata: Dispose() edildikten sonra NativeHashMap‘e erişmeye çalışmak. Bu durum, uygulamanın çökmesine veya tanımsız davranışlara yol açabilir.

Çözüm: Her erişimden önce IsCreated kontrolünü kullanın. Özellikle uzun ömürlü koleksiyonlarda bu kontrol çok önemlidir.

3. Yanlış `Allocator` Kullanımı

Hata: Uzun ömürlü veriler için Allocator.Temp kullanmak veya kısa ömürlü veriler için Allocator.Persistent kullanıp performanstan ödün vermek.

Çözüm: Koleksiyonun yaşam döngüsüne uygun Allocator tipini seçin. Çoğu MonoBehaviour tabanlı kullanım için Allocator.Persistent ve OnDestroy() içinde Dispose() idealdir. Jobs için Allocator.TempJob sıkça kullanılır.

4. Main Thread’de Aşırı Kullanım

Hata: NativeHashMap‘i sadece main thread’de Dictionary yerine kullanmaya çalışmak ve performans artışı beklemek.

Çözüm: NativeHashMap‘in asıl avantajı Jobs sistemi ile paralel işlemlerde ortaya çıkar. Main thread’de tek başına kullanıldığında, Dictionary‘ye göre belirgin bir performans farkı yaratmayabilir, hatta bazı durumlarda daha yavaş olabilir (özellikle küçük veri setlerinde ve sık bellek tahsis/serbest bırakma durumlarında). Yalnızca gerçekten paralel işlem gerektiren veya GC’den tamamen kaçınmanız gereken durumlarda tercih edin.

Performans ve Optimizasyon Notları

  • Burst Derleyici: NativeHashMap, Burst derleyici ile mükemmel bir uyum içindedir. Burst, Jobs sistemindeki kodunuzu yüksek performanslı makine koduna dönüştürerek NativeHashMap işlemlerinin inanılmaz derecede hızlı çalışmasını sağlar.
  • Ön-Tahsis (Pre-allocation): NativeHashMap‘i başlangıçta yeterli kapasiteyle oluşturmak, çalışma zamanında yeniden boyutlandırma (re-hashing) maliyetinden kaçınmanızı sağlar. Bu, özellikle oyun döngüsü içinde sıkça kullanılan koleksiyonlar için kritik bir optimizasyondur.
  • Cache Locality: Unmanaged bellek ve değer tipleri sayesinde NativeHashMap, veri yerelliğini (cache locality) artırabilir. Bu, CPU önbelleğinin daha verimli kullanılmasına ve verilere daha hızlı erişilmesine olanak tanır.
  • Sadece İhtiyaç Duyulduğunda Kullanın: Her zaman NativeHashMap kullanmak zorunda değilsiniz. Eğer veri setiniz küçükse, paralel işlem gereksiniminiz yoksa veya GC duraklamaları projeniz için bir sorun teşkil etmiyorsa, standart Dictionary daha basit ve yeterli bir çözüm olabilir.

NativeHashMap, Unity’de yüksek performanslı, veri odaklı tasarımlar (DOTS) geliştirirken vazgeçilmez bir araçtır. Bellek yönetimi konusunda dikkatli olduğunuz sürece, oyunlarınızın performansını önemli ölçüde artırabilirsiniz.

Leave a Reply

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