Modern .NET'in performans hikayesinin büyük bölümü Span<T> ve Memory<T>'ye dayanıyor. Bu tipler, dizi kopyalamadan bellek dilimleri üzerinde çalışmanızı sağlayarak hem GC baskısını hem de CPU zamanını önemli ölçüde azaltıyor.
Sorun: Gereksiz Kopya ve Allocation
// Klasik yaklaşım — her operasyon yeni string/dizi oluşturur
string ParseToken(string header)
{
// "Bearer eyJhbGci..." → "eyJhbGci..."
if (header.StartsWith("Bearer "))
return header.Substring(7); // Yeni string! Heap allocation.
return string.Empty;
}
// 1000 req/sn geliyorsa: saniyede 1000 gereksiz string nesnesi
Span<T>: Stack'te Yaşayan Dilim
Span<T> bir struct'tır ve her zaman stack'te yaşar. Heap'teki veriyi kopyalamadan referans alır:
// Sıfır allocation ile aynı iş
ReadOnlySpan<char> ParseToken(ReadOnlySpan<char> header)
{
const string prefix = "Bearer ";
if (header.StartsWith(prefix.AsSpan()))
return header.Slice(prefix.Length); // Yeni nesne yok, sadece pointer+length
return ReadOnlySpan<char>.Empty;
}
// Kullanım
string authHeader = "Bearer eyJhbGciOiJSUzI1NiJ9....";
ReadOnlySpan<char> token = ParseToken(authHeader.AsSpan());
// token, authHeader'ın heap'teki verisini gösteriyor — kopya yok
stackalloc ile Gerçek Stack Belleği
// Küçük buffer'lar için heap allocation gerekmez
void ProcessPacket(ReadOnlySpan<byte> data)
{
// 256 byte'a kadar stack'te tut, daha büyükse heap'e geç
Span<byte> buffer = data.Length <= 256
? stackalloc byte[data.Length]
: new byte[data.Length];
data.CopyTo(buffer);
// buffer üzerinde çalış — scope bitince otomatik temizlenir
XorBuffer(buffer, key: 0x42);
SendToNetwork(buffer);
}
// Pratik örnek: Base64 encode — heap allocation yok
string EncodeToBase64(ReadOnlySpan<byte> data)
{
int base64Length = ((data.Length + 2) / 3) * 4;
Span<char> output = base64Length <= 512
? stackalloc char[base64Length]
: new char[base64Length];
Convert.TryToBase64Chars(data, output, out int written);
return new string(output[..written]);
}
String Parsing: MemoryExtensions ile
// CSV satırı parse etme — sıfır allocation
void ParseCsvRow(ReadOnlySpan<char> line)
{
int fieldStart = 0;
for (int i = 0; i <= line.Length; i++)
{
if (i == line.Length || line[i] == ',')
{
var field = line.Slice(fieldStart, i - fieldStart).Trim();
ProcessField(field); // Span<char> — kopya yok
fieldStart = i + 1;
}
}
}
// Sayı parse etme — string oluşturulmadan
bool TryParsePort(ReadOnlySpan<char> input, out int port)
=> int.TryParse(input, out port) && port is > 0 and <= 65535;
// Kullanım
var config = "host:8080".AsSpan();
int colonIdx = config.IndexOf(':');
var host = config[..colonIdx];
TryParsePort(config[(colonIdx + 1)..], out int port);
Memory<T>: Async ile Uyumlu Span
Span<T> stack-only olduğu için async metotlarda, field'larda veya lambda'larda kullanılamaz. Bu durumlarda Memory<T> kullanılır:
// Span async metotta kullanılamaz — derleme hatası
async Task ProcessAsync(Span<byte> data) { } // Hata!
// Memory async uyumlu
async Task ProcessAsync(Memory<byte> buffer, CancellationToken ct)
{
// Stream'den oku
int bytesRead = await _stream.ReadAsync(buffer, ct);
// Span'e dönüştür (senkron işlem için)
var data = buffer.Span[..bytesRead];
var checksum = ComputeChecksum(data);
await _output.WriteAsync(buffer[..bytesRead], ct);
}
// IMemoryOwner ile havuzdan bellek al
async Task ProcessLargeFileAsync(string path)
{
// ArrayPool üzerinden — GC'ye değil pool'a geri döner
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
Memory<byte> buffer = owner.Memory;
await using var file = File.OpenRead(path);
int bytesRead;
while ((bytesRead = await file.ReadAsync(buffer)) > 0)
await ProcessAsync(buffer[..bytesRead], CancellationToken.None);
}
ArrayPool<T>: Buffer'ları Yeniden Kullan
// Büyük geçici diziler için ArrayPool
async Task<string> CompressAndEncodeAsync(byte[] data)
{
// Pool'dan al — new byte[] yerine
byte[] rentedBuffer = ArrayPool<byte>.Shared.Rent(data.Length * 2);
try
{
using var ms = new MemoryStream(rentedBuffer);
using var gz = new GZipStream(ms, CompressionLevel.Fastest);
await gz.WriteAsync(data);
await gz.FlushAsync();
return Convert.ToBase64String(rentedBuffer, 0, (int)ms.Position);
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer); // Pool'a geri ver
}
}
Kısıtlamalar
Span<T>ref struct'tır: class field'ı olamaz, boxing edilemez, lambda'da yakalanamaz- Async metotlarda Span yerine Memory kullanın
- stackalloc için genellikle 1 KB altı tavsiye edilir; daha büyük boyutlar için ArrayPool
Memory<T>.Spanözelliği senkron context'te kullanılabilir
Span<T> ve Memory<T> ilk kullanımda biraz alışma gerektiriyor ama özellikle ağ protokolü implementasyonları, dosya parsing işlemleri ve yüksek frekanslı servislerde GC baskısını dramatik biçimde azaltıyor. .NET runtime'ın kendisi de bu API'leri yoğun kullanıyor.