C# PERFORMANS SPAN VE MEMORY
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.
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.
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.

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);
}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.
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));
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 structolduğ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
stackallockapsamı 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.



