.NET Performans & Bellek Yönetimi 4 dk okuma 12 okunma

Object Pooling ve ArrayPool<T> Kullanımı

Object Pooling, sık oluşturulup yok edilen nesneleri yeniden kullanarak GC baskısını azaltır. ArrayPool<T>, büyük diziler için kullanıma hazır ve thread-safe bir havuz sunar. Microsoft.Extensions.ObjectPool ise özel nesneler için esnek bir altyapı sağlar. Ancak pooling, her senaryoya uygun değildir: yanlış kullanıldığında bellek sızıntısına veya veri sızıntısına yol açabilir.

Object Pooling ve ArrayPool<T> Kullanımı

Object Pooling ve ArrayPool<T> Kullanımı

.NET'te Garbage Collector (GC), bellek yönetimini kolaylaştırır ancak ücretsiz değildir. Özellikle yüksek trafikli sistemlerde, sık oluşturulup kısa süre içinde çöp olan nesneler GC baskısını artırır; bu da gecikme sıçramalarına ve CPU tüketimine yol açar. Object Pooling (nesne havuzu), nesneleri yok etmek yerine yeniden kullanarak bu baskıyı azaltır. ArrayPool<T> ise .NET'in bu ihtiyaç için sunduğu, kullanıma hazır ve thread-safe çözümdür.


1. Object Pooling Nedir ve Ne Zaman Kullanılır?

Object Pooling, bir nesne oluşturma maliyeti yüksekse veya nesne çok sık oluşturulup yok ediliyorsa, nesneleri bir havuzda tutup yeniden kullanma tekniğidir.

Kullanım Alanları:

  • Büyük diziler: new byte[8192] gibi tahsisler, LOH (Large Object Heap) üzerinde GC baskısı yaratır.

  • Pahalı başlatma gerektiren nesneler: StringBuilder, HTTP bağlantıları, şifreleme nesneleri.

  • Sık kullanılan geçici nesneler: Kısa ömürlü buffer'lar, formatlama nesneleri.

Ne Zaman Kullanılmamalı?

  • Nesne küçük ve oluşturma maliyeti düşükse (ör. küçük POCO'lar).

  • Nesne durum (state) tutuyorsa ve sıfırlanması zorsa.

  • Havuz, bellek sızıntısına dönüşme riski taşıyorsa.

Kritik Kural: Havuzdan alınan nesne, kullanım sonrası mutlaka havuza geri bırakılmalıdır. Aksi halde havuz büyür, bellek sızıntısına yol açar.


2. ArrayPool<T>: Diziler için Hazır Çözüm

System.Buffers.ArrayPool<T>, .NET'in yerleşik dizi havuzudur. Shared özelliği ile thread-safe bir singleton havuz sunar.

Temel Kullanım:

csharp

var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
    // buffer üzerinde çalış
    int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct);
    Process(buffer, bytesRead);
}
finally
{
    // Her durumda havuza geri bırak
    ArrayPool<byte>.Shared.Return(buffer);
}

Kritik Detaylar:

  • Rent Boyutu: Rent(8192) çağrısı, en az 8192 boyutunda bir dizi döner; ancak dönen dizi daha büyük olabilir. Bu nedenle buffer.Length yerine, gerçekten kullandığınız uzunluğu takip edin .

  • Return ve Temizlik: Varsayılan olarak Return, diziyi temizlemez. Hassas veri (şifre, token) tutuyorsanız Return(buffer, clearArray: true) kullanın .

  • Özel Havuz: ArrayPool<T>.Create(maxArrayLength, maxArraysPerBucket) ile kendi havuzunuzu oluşturabilirsiniz; ancak çoğu senaryoda Shared yeterlidir.

Ne Zaman ArrayPool?

  • 1KB üzeri geçici buffer'larda.

  • Ağ akışları, dosya okuma, serileştirme gibi I/O işlemlerinde.

  • stackalloc için çok büyük olan dizilerde (stackalloc tipik olarak <1KB için uygundur).


3. Microsoft.Extensions.ObjectPool: Özel Nesneler için Havuz

Diziler dışındaki nesneler için .NET, Microsoft.Extensions.ObjectPool paketini sunar. Bu paket, ASP.NET Core ve diğer Microsoft kütüphanelerinde yaygın olarak kullanılır.

Temel Kullanım:

csharp

using Microsoft.Extensions.ObjectPool;

// Politika tanımla
var policy = new DefaultPooledObjectPolicy<StringBuilder>();
var pool = new DefaultObjectPool<StringBuilder>(policy);

// Havuzdan al
var sb = pool.Get();
try
{
    sb.Append("Merhaba");
    Console.WriteLine(sb.ToString());
}
finally
{
    // Havuza geri bırak (içeriği temizler)
    pool.Return(sb);
}

DefaultPooledObjectPolicy: StringBuilder için özel bir politika; Return çağrısıldığında StringBuilder'ın içeriğini temizler .

Özel Politika:

csharp

public class MyObjectPolicy : IPooledObjectPolicy<MyObject>
{
    public MyObject Create() => new MyObject();
    
    public bool Return(MyObject obj)
    {
        obj.Reset(); // Durumu sıfırla
        return true; // Havuza geri kabul et
    }
}

DI Entegrasyonu (ASP.NET Core):

csharp

builder.Services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.AddSingleton(sp =>
{
    var provider = sp.GetRequiredService<ObjectPoolProvider>();
    return provider.Create<StringBuilder>();
});

4. ArrayPool vs. ObjectPool: Hangisi Ne Zaman?

Kriter ArrayPool<T> ObjectPool<T>
Hedef Diziler (T[]) Herhangi bir referans tipi
Thread-Safety Shared thread-safe Politikaya bağlı
Kullanım Kolaylığı Çok basit (Rent/Return) Politika tanımı gerekir
Temizlik Varsayılan olarak temizlemez Politikaya bağlı
Tipik Kullanım Byte buffer, char buffer StringBuilder, geçici nesneler

Pratik Kural: Dizi gerekiyorsa ArrayPool<T>; özel bir nesne gerekiyorsa ObjectPool<T> kullanın.


5. Gerçek Dünya Örneği: Yüksek Trafikli API'de Buffer Yönetimi

Bir dosya yükleme API'sinde, her istek 8KB'lık bir buffer tahsis ediyor. Saniyede 1.000 istek geldiğinde, bu saniyede 8MB ek tahsis anlamına gelir ve GC sürekli çalışır.

Önce (Naive):

csharp

var buffer = new byte[8192]; // Her istekte yeni tahsis
await stream.ReadAsync(buffer);

Sonra (ArrayPool):

csharp

var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
    int read;
    while ((read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct)) > 0)
    {
        await ProcessAsync(buffer.AsMemory(0, read), ct);
    }
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

Sonuç: Saniyede 8MB tahsis yerine, havuzdaki birkaç buffer yeniden kullanılır; GC baskısı neredeyse sıfıra iner.


6. En İyi Pratikler ve Tuzaklar

Pratik / Tuzak Açıklama
Her zaman finally ile Return İstisna durumunda bile buffer havuza dönmelidir; aksi halde havuz tükenir .
Rent boyutunu varsaymayın Rent(n) en az n döner; dönen dizinin gerçek boyutunu kontrol edin .
Hassas veriyi temizleyin Return(buffer, clearArray: true) ile şifre/token kalıntılarını silin .
Havuzdan alınan nesneyi sıfırlayın Özellikle ObjectPool'da, önceki kullanımdan kalan durum sızabilir.
Havuzu aşırı büyütmeyin ArrayPool.Create ile maksimum boyut belirleyin; sınırsız havuz bellek sızıntısıdır .
Küçük nesneler için pooling yapmayın Küçük POCO'lar için pooling, GC'den daha maliyetli olabilir.

Sonuç

Object Pooling ve ArrayPool<T>, .NET'te GC baskısını azaltmanın en etkili yollarından biridir. ArrayPool<T>, özellikle büyük byte/char buffer'lar için kullanıma hazır, thread-safe bir çözüm sunar; Microsoft.Extensions.ObjectPool ise özel nesneler için esnek bir altyapı sağlar. Ancak pooling, "her yerde kullanılacak" bir teknik değildir: nesne küçükse veya durum yönetimi zorsa, faydadan çok zarar getirebilir. Doğru kullanıldığında, yüksek trafikli sistemlerde ölçülebilir gecikme ve CPU iyileşmeleri sağlar. Unutmayın: Havuzdan aldığınız her nesneyi geri bırakmak, bir disiplindir; bu disiplin olmadan pooling, bellek sızıntısına dönüşür.

Tüm yazılar Soru ya da yorumun mu var? Yaz