C# PERFORMANS SPAN VE MEMORY

Veri Akademi9 dk okuma
Bitişik bellek hücreleri üzerinde dört hücreyi vurgulayan parantezli Span penceresi ve görüntüleme oku

Bir .NET API'sini profile eden senior developer hot path'te CPU zamanının %30'unu GC içinde harcandığını fark eder. Sebep, request başına atılan onlarca string.Substring çağrısıdır — her biri heap üzerinde yeni bir string allocation üretir, GC bunları toplamak zorunda kalır. Refactor sonrası aynı kod ReadOnlySpan<char> üzerinden çalışır. GC zamanı %2'ye iner, throughput iki katına çıkar.

Bu senaryo abartı değil; high-throughput sistemlerde tipik bir kazanç tablosu. Span<T> ve Memory<T> C# 7.2 ile geldi ve performans odaklı kod yazan herkes için artık temel araç sayılır. İlk bakışta lüks bir özellik gibi durur — neden basit indeksleme yerine ayrı bir tip? Çünkü Span allocation üretmez, GC yükünü minimize eder ve sıcak kod yollarında dramatik fark yaratır. Aşağıdaki bölümler bu iki tipin ne olduğunu, aralarındaki farkı, hangi pratik durumlarda işe yaradığını ve dikkat edilmesi gereken sınırları sırayla ele alıyor.

1. Allocation Maliyeti ve GC Yükü

.NET'te new ile her nesne oluşturulduğunda heap üzerinde bellek ayrılır. Allocation kendisi ucuzdur — pointer ileri kaydırma neredeyse bedavadır. Asıl maliyet daha sonra gelir: Garbage Collector dolan generation'ı temizlerken çalışmayı durdurur (stop-the-world), referansları gezer, ölü nesneleri yok eder. Saniyede binlerce isteğin geçtiği bir servisde bu duraklamalar latency tail'inde net görünür.

String işlemleri bu maliyetin en sık görüldüğü yerdir. Klasik Substring, Split, Trim çağrılarının tamamı yeni string nesnesi üretir. Bir CSV parser düşünün: bir satır okunur, virgülden ayrılır, her hücre trim edilir, bir alan parse edilir. Tek satır için belki 10 allocation. 100 bin satırlık dosyada bir milyon allocation. Span ile aynı iş sıfır allocation ile yapılır.

2. Span<T> Nedir

Span<T> herhangi bir bitişik bellek bölgesine — array, string, stackalloc veya unmanaged buffer — sıfır allocation ile referans tutan bir ref struct'tır. Aslında basit bir kavram: bir pointer + uzunluk. Ama tip sistemi sayesinde güvenli, GC ile uyumlu ve gerçek bir bellek bölgesini referans gibi davranır.

Üç önemli özelliği vardır. Birincisi sıfır allocation: bir Span oluşturmak heap'e dokunmaz, sadece stack'te kalır. İkincisi tek tip API: kaynağı array, string veya unmanaged bellek olsun fark etmez — aynı metotlarla erişilir. Üçüncüsü slice ucuzdur: bir Slice(start, length) çağrısı kopya üretmez, sadece yeni bir pencere açar.

ReadOnlySpan<char> input = "name=ahmet&age=30".AsSpan();
int equalsIdx = input.IndexOf('=');
ReadOnlySpan<char> key = input.Slice(0, equalsIdx);
ReadOnlySpan<char> value = input.Slice(equalsIdx + 1);

Yukarıdaki kod parçası iki "alt string" oluşturur ama heap'e tek bir bayt dokunmaz. key ve value orijinal string'in içindeki pencerelerdir. Tüm API yüzeyi resmi bellek ve span belgelerinde ayrıntılı listelenir.

Stack ve heap karşılaştırması: solda stack frame üstünde Span ve sıfır allocation rozeti, sağda heap'te yığın ve GC süpürme oku

3. Memory<T> Nedir

Memory<T> de bitişik bellek bölgesine referans tutar; ancak Span<T>'in aksine normal bir struct'tır, ref struct değildir. Pratik fark: Memory async metodlarda saklanabilir, alanlarda tutulabilir, heap'e konabilir. Span ise yalnızca stack'te yaşar.

Yani Memory, Span'in kullanılamadığı yerlerde devreye girer. Async bir metot içinde await sonrası bellek bölgesine erişmek istiyorsanız Span çalışmaz çünkü stack frame yok olur. Memory bu boşluğu doldurur. İhtiyaç anında memory.Span ile geçici bir Span elde edilir, senkron işlem o pencere içinde yapılır.

public async Task ProcessAsync(ReadOnlyMemory<byte> buffer)
{
    await SomeAsyncCall();
    ReadOnlySpan<byte> span = buffer.Span;
    ProcessSync(span);
}

4. Span ve Memory Arasındaki Fark

İki tip benzer görünür ama kullanım sınırları belirgin biçimde farklıdır. Karar verirken aşağıdaki tabloyu zihinde tutmak yeterlidir:

  • Span<T> — sadece stack, async/await içinde saklanamaz, field olamaz, sıfır overhead, en hızlı erişim
  • Memory<T> — heap'e gidebilir, async metodlarda saklanır, field olabilir, ek bir dolaylı erişim katmanı taşır
  • ReadOnlySpan / ReadOnlyMemory — değiştirilemez varyantları; string ve immutable buffer'larla doğal uyum sağlar
  • Pratik kural — senkron hot path'te Span tercih; async sınırını geçecek bellek bölgesi için Memory

Performans karakteristiği de farklıdır. Span'e erişim doğrudan pointer aritmetiğine derlenir; JIT bunları intrinsic olarak tanır ve bounds check elemine kadar gider. Memory üzerinde aynı işlem önce .Span property'sini çağırmayı gerektirir; mikrosaniye ölçeğinde fark vardır.

5. Pratik Kullanım Örnekleri

Span en çok parser ve serileştirme kodunda fark yaratır. Bir log dosyasını satır satır okuyup her satırdan tarih, level ve mesaj çıkaran bir parser düşünün. Klasik yaklaşım her satır için 3-4 string allocation üretir. Span tabanlı versiyon orijinal buffer üzerinde slice'lar ile çalışır — tek bir allocation yok.

Diğer yaygın senaryo network protokollerinde paket parse etmektir. TCP üzerinden gelen byte buffer'ı header, payload, checksum bölümlerine ayrılır. ReadOnlySpan<byte> ile her bölüm aynı buffer üzerinde bir pencere olarak işlenir. ASP.NET Core'un Kestrel sunucusu bu yaklaşımı yoğun biçimde kullanır.

Sayısal hesaplama tarafında da kullanım alanı geniştir. Büyük bir array üzerinde belirli aralıkta toplama, ortalama, transform işlemleri Span ile slice'lanır ve Span<T>.CopyTo intrinsic'leri SIMD desteğiyle hızlanır. Performans yönlü C# pratiğine yapılandırılmış biçimde girmek isteyenler .NET test ve performans eğitimi üzerinden bu konuları uygulamalı işleyebilir.

stackalloc ile Birleşim

Küçük geçici buffer'lar için stackalloc + Span ikilisi muazzam etkilidir. Aşağıdaki örnek 256 byte'lık geçici bir buffer'ı stack'te alır, hiç heap'e dokunmaz:

Span<byte> buffer = stackalloc byte[256];
int bytesWritten = Encoding.UTF8.GetBytes(source, buffer);
ProcessBuffer(buffer.Slice(0, bytesWritten));
Substring ve Span.Slice bar grafik karşılaştırması, solda yüksek çubuklar sağda kısa çubuklar ile iyileşme oku

6. Tuzaklar ve Sınırlar

Span güçlüdür ama kısıtlamaları net biçimde bilinmezse derleyici hatalarıyla zaman kaybedilir. En sık karşılaşılan durumlar şunlardır:

  • ref struct olduğu için class alanı, generic type argument veya box edilebilir tip içinde tutulamaz
  • Async metotta await öncesi/sonrası saklanamaz — bu senaryo için Memory kullanılır
  • Yield return ile döndürülemez (iterator state machine ref struct tutamaz)
  • Boxing veya reflection üzerinden geçirilemez
  • Underlying buffer yok olursa Span dangling pointer durumuna düşer — örneğin stackalloc kapsamı dışında kullanılırsa

Bir diğer tuzak premature optimization'dır. Span her yerde performans getirmez. Sıcak olmayan bir kod yolu için Span'e geçmek kodu zorlaştırır ama ölçülebilir kazanç sağlamaz. Önce profile edin, sonra kararı verin. C# eğitim içeriği boyunca temel dil yapılarını oturtmuş geliştiriciler için Span doğal bir sonraki adımdır; bu sırayı atlamak verimsizdir.

Son olarak hata mesajları başta kafa karıştırıcı gelebilir. "Cannot use ref local 'span' inside an anonymous method, lambda expression, or query expression" gibi mesajlar Span'in stack-only doğasından kaynaklanır. Mesaj anlaşıldığında çözüm genelde Memory'ye geçmek veya kapsama göre yeniden yapılandırmaktır.

Sıkça Sorulan Sorular

Span ve Memory hangi C# sürümüyle geldi?

Span&lt;T&gt; ve Memory&lt;T&gt; resmi olarak C# 7.2 ve .NET Core 2.1 ile geldi. Tam intrinsic desteği ve performans kazançları .NET Core 3.0 sonrasında belirgin biçimde olgunlaştı. Bugün .NET 6 ve üzeri sürümler bu tipleri framework genelinde yoğun kullanır.

Span ile string.Substring arasındaki performans farkı ne kadar?

Tipik bir parsing senaryosunda 5-20 kat hız farkı görülür. Asıl kazanç allocation azalmasından gelir: Substring her çağrıda yeni nesne üretirken Span.Slice sıfır allocation yapar. GC baskısı azaldıkça throughput artar, latency tail iyileşir.

Span async metotta neden kullanılamaz?

Span ref struct olduğu için await sınırını geçemez. Await ile metot state machine'e dönüştürülür ve lokal değişkenler heap'e taşınabilir; ref struct heap'e gidemediği için derleyici bunu engeller. Bu durumlarda Memory&lt;T&gt; kullanılır.

Span üzerinde LINQ kullanabilir miyim?

Doğrudan LINQ extension'ları Span üzerinde çalışmaz çünkü LINQ IEnumerable üzerinden çalışır ve Span IEnumerable implement edemez. Bunun yerine Span üzerine inşa edilmiş özel API'ler veya manuel döngü kullanılır. Bazı topluluk kütüphaneleri Span-uyumlu LINQ benzeri yapılar sunar.

stackalloc her zaman güvenli midir?

Hayır. stackalloc stack üzerinde bellek ayırır; çok büyük tahsisler stack overflow'a neden olabilir. Genel pratik 1KB altında tutmak veya CLR'nin tanıdığı 1 KB sınırının altında kalmaktır. Büyük buffer için ArrayPool veya heap allocation tercih edilir.

ReadOnlySpan ile Span arasındaki fark nedir?

ReadOnlySpan&lt;T&gt; referans tuttuğu bölgeye yazma izni vermez, sadece okumaya açar. String'ten elde edilen tüm span'lar zorunlu olarak ReadOnly'dir çünkü string immutable'dır. Tasarımda intent'i belirtmek ve istemsiz mutasyonu önlemek için tercih edilir.

ArrayPool ile Span birlikte nasıl kullanılır?

ArrayPool&lt;T&gt;.Shared.Rent ile havuzdan bir array kiralanır, üzerine Span açılır, iş bitince Return ile geri verilir. Bu desen büyük ama geçici buffer ihtiyacında allocation üretmez. Özellikle JSON ve protokol parser'larında yaygındır.