.NET'te Span<T> ve Memory<T> ile Düşük Seviye Optimizasyon
.NET ekosisteminde performans darboğazlarının büyük çoğunluğu, gereksiz bellek tahsislerinden (allocation) kaynaklanır. Her new çağrısı, Garbage Collector (GC) üzerinde baskı oluşturur; yoğun trafikli bir API'de bu baskı, gecikme sıçramalarına ve CPU tüketimine dönüşür. Span<T> ve Memory<T>, tam bu noktada devreye girer: mevcut belleğin bir bölümüne kopyalama yapmadan erişmenizi sağlayarak, sıfır tahsisli (zero-allocation) kod yazmanıza olanak tanır .
1. Span<T> ve Memory<T> Nedir?
Her iki yapı da bitişik bir bellek bloğunun görünümünü (view) temsil eder. Bir dizi, string veya stack üzerinde ayrılmış bir alanın belirli bir kesitine, veriyi kopyalamadan erişirsiniz .
Temel Farklar:
| Özellik | Span<T> | Memory<T> |
|---|---|---|
| Bellek Konumu | Yalnızca stack (ref struct) | Heap'te saklanabilir |
| Asenkron Uyumluluk | ❌ await sınırını geçemez |
✅ Asenkron metotlarda kullanılabilir |
| Kullanım Alanı | Senkron işlemler, parsing, formatlama | Asenkron I/O, sınıf alanı, cache |
| Sınırlama | Sınıf alanı olamaz, boxing yapılamaz | ArraySegment ile uyumludur |
Neden Önemli? Substring veya Split gibi geleneksel string metotları, her çağrıda yeni bir string nesnesi oluşturur. Bir log satırından request ID çıkarmak gibi milyonlarca kez tekrarlanan bir işlemde bu, GC'yi sürekli meşgul eder. Span<T>.Slice ise aynı işlemi hiç tahsis yapmadan gerçekleştirir; benchmark sonuçlarına göre Substring'e kıyasla 7,5 kata kadar daha hızlıdır .
2. Span<T> ile Zero-Allocation Teknikleri
A. String Parsing (Slice ile)
csharp
public bool TryParseKeyValue(ReadOnlySpan<char> line, out string key, out string value)
{
key = string.Empty;
value = string.Empty;
int colonIndex = line.IndexOf(':');
if (colonIndex == -1) return false;
// Sadece format doğrulandıktan sonra string tahsis et
key = new string(line.Slice(0, colonIndex).Trim());
value = new string(line.Slice(colonIndex + 1).Trim());
return true;
}
Bu desen, özellikle HTTP header'ları, log satırları ve CSV parsing'de kritiktir .
B. stackalloc ile Yığın Üzerinde Geçici Buffer
csharp
Span<char> buffer = stackalloc char[256]; var written = FormatInto(buffer); var message = new string(buffer.Slice(0, written));
stackalloc, heap yerine stack üzerinde bellek ayırır; GC baskısı sıfırdır. Dikkat: [SkipLocalsInit] özniteliği, .NET'in varsayılan sıfırlama işlemini atlar; ancak buffer'a yazmadan okuma yaparsanız çöp veri alırsınız. Sadece profilleme ile sıfırlamanın darboğaz olduğu kanıtlanmışsa kullanın .
C. ArrayPool ile Büyük Geçici Buffer'lar
stackalloc yalnızca küçük boyutlar için uygundur (tipik olarak <1KB). Daha büyük buffer'lar için ArrayPool<T>.Shared kullanın:
csharp
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(), ct)) > 0)
{
// buffer üzerinde çalış
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
Bu desen, büyük dosya işleme veya ağ akışlarında GC'yi korur .
3. Memory<T> ile Asenkron İşlemler
Span<T> bir ref struct olduğu için await sınırını geçemez. Asenkron I/O işlemlerinde Memory<T> kullanılır:
csharp
public async Task<int> ReadDataAsync(Memory<byte> buffer, CancellationToken ct)
{
return await _stream.ReadAsync(buffer, ct);
}
Memory<T>, Span<T>'e dönüştürülebilir (memory.Span), ancak tersi mümkün değildir .
4. Kritik Kullanım Kuralları
Microsoft'un resmi yönergelerine göre :
-
Kural #1: Senkron API'lerde parametre olarak
Span<T>tercih edin;Memory<T>'den daha esnek ve performanslıdır. -
Kural #2: Buffer salt okunur olacaksa
ReadOnlySpan<T>veyaReadOnlyMemory<T>kullanın. Bu, derleyiciye optimizasyon fırsatı verir. -
Kural #3:
Memory<T>parametresi alan bir metot,voiddöndüğünde buffer'ı artık kullanmamalıdır; aksi halde sahiplik ihlali oluşur. -
Sahiplik (Ownership): Bir buffer'ın her zaman bir sahibi vardır.
IMemoryOwner<T>ile açık sahiplik yönetimi yapılabilir; sahipsiz örneklerde GC örtük sahip sayılır .
5. Gerçek Dünya Etkisi
Brezilya'daki bir üniversite çalışmasında, finansal veri işleme (banka işlemleri ve BoletoNet kütüphanesi) senaryolarında Span<T> ve Memory<T> kullanımı test edilmiştir. Sonuçlar: birden fazla senaryoda sıfır ek kopya, belirgin hız artışı ve yanıt sürelerinde daha yüksek kararlılık . Bir log parsing benchmark'ında ise Span tabanlı Slice, Substring'e göre 7,5 kat daha hızlı ve sıfır allocation üretmiştir .
6. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Sıcak yollarda Span kullanın | HTTP pipeline, JSON parser, log işleme gibi yüksek frekanslı alanlarda . |
| Asenkron için Memory kullanın | await geçen metotlarda Span yerine Memory tercih edin . |
| ReadOnly tercih edin | Değiştirilmeyecek buffer'larda ReadOnlySpan/ReadOnlyMemory kullanın . |
| ArrayPool ile büyük buffer yönetin | 1KB üzeri geçici buffer'larda ArrayPool kullanın . |
| stackalloc'u sınırlı tutun | Yalnızca küçük, kısa ömürlü buffer'larda ve SkipLocalsInit riskini bilerek kullanın . |
Sonuç
Span<T> ve Memory<T>, .NET'te düşük seviye optimizasyonun temel araçlarıdır. Span<T>, senkron ve stack tabanlı işlemlerde sıfır tahsisli erişim sağlarken; Memory<T>, asenkron akışlarda ve heap'te saklanması gereken buffer'larda aynı avantajı sunar. Doğru kullanıldığında, GC baskısını dramatik biçimde azaltır ve yüksek trafikli sistemlerde ölçülebilir gecikme iyileşmeleri sağlar. Ancak bu araçlar, "her yerde kullanılacak" gümüş kurşunlar değildir; sahiplik kurallarına ve senkron/asenkron ayrımına dikkat edilmezse, çalışma zamanı hatalarına yol açabilir. Unutmayın: Optimizasyon, ölçümle başlar; Span<T> ve Memory<T>, ölçüm sonrası en güçlü silahınızdır.