Performance-critical code zawsze walczył z alokacjami w heap - każda alokacja to praca dla garbage collector. W 2015 roku nie miałeś wyboru - wszystko na heapie. W 2026 roku masz Span<T> i Memory<T> - zero-allocation code!
W tym wpisie poznasz Span<T> - stack-based slice of memory, Memory<T> - heap-safe alternative, stackalloc, implicit span conversions (C# 14), i praktyczne zastosowania w string parsing, data processing, i high-performance scenarios!
📅 Timeline - ewolucja Span i Memory
C# 1.0-6.0 (do 2015) - Tylko heap allocations, brak alternatywy
C# 7.2 (2017) - 🔥 Span<T> i Memory<T> introduced!
C# 7.2 (2017) - stackalloc z Span<T>
C# 8.0 (2019) - Ranges i indices z Span
C# 11 (2022) - Improved span patterns
C# 14 (2026) - 🔥 Implicit span conversions
Problem - heap allocations wszędzie
Alokacje w heap - performance killer
// Problem: każda operacja alokuje w heap!
string text = "Hello World from C#";
// Substring - alokacja!
string sub1 = text.Substring(0, 5); // "Hello" - nowy string w heap
string sub2 = text.Substring(6, 5); // "World" - nowy string w heap
// Split - wiele alokacji!
string[] words = text.Split(' '); // Array + każdy string = 5 alokacji!
// ToUpper - alokacja!
string upper = text.ToUpper(); // Nowy string w heap
// W pętli to katastrofa:
for (int i = 0; i < 1000000; i++)
{
string s = text.Substring(0, 5); // 1 milion alokacji! 😱
// Garbage collector: "Am I a joke to you?"
}
Konsekwencje heap allocations
// Przykład: CSV parsing
string csv = "Jan,Kowalski,30,Warsaw\nAnna,Nowak,25,Krakow";
// ❌ Stare podejście - wiele alokacji
string[] lines = csv.Split('\n'); // Alokacja array + strings
foreach (string line in lines)
{
string[] fields = line.Split(','); // Alokacja array + strings
string name = fields[0]; // Reference
string surname = fields[1]; // Reference
int age = int.Parse(fields[2]); // Parse
string city = fields[3]; // Reference
// Dla 2 linii: 2 + 2*4 = 10 alokacji w heap!
}
// Konsekwencje:
// 1. Memory pressure - GC musi sprzątać
// 2. GC pauses - zatrzymanie aplikacji
// 3. CPU cycles - GC zabiera czas CPU
// 4. Cache misses - heap nie jest cache-friendly
🔍 Stack vs Heap
Stack - szybki, automatyczne cleanup, ograniczony rozmiar (~1MB) Heap - wolniejszy, wymaga GC, praktycznie nieograniczony
Span<T> pozwala używać stack dla operacji które normalnie wymagałyby heap!
🔥 Span<T> - zero-allocation slices (C# 7.2)
Czym jest Span<T>?
🎉 C# 7.2 - Span<T>
Span<T> to "view" na ciągły obszar pamięci - może wskazywać na stack, heap, lub native memory. Zero allocations!
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Kopiuje elementy - alokacja!
int[] slice = numbers[2..7]; // [3, 4, 5, 6, 7]
// Nowy array w heap
// GC będzie musiał to posprzątać
✅ Span slicing - zero allocations
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Wskazuje na część array - BEZ alokacji!
Span<int> slice = numbers.AsSpan(2, 5);
// Tylko referencja do pamięci
// Brak GC pressure! ✨
Span<T> operations
// Span operations - wszystkie bez alokacji!
int[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Span<int> span = data.AsSpan();
// Length
int length = span.Length; // 10
// Indexing
int first = span[0]; // 1
int last = span[^1]; // 10 (index from end)
// Slicing z range operator
Span<int> slice1 = span[2..5]; // [3, 4, 5]
Span<int> slice2 = span[..3]; // [1, 2, 3]
Span<int> slice3 = span[5..]; // [6, 7, 8, 9, 10]
// Clear - zerowanie
span.Clear(); // Wszystkie elementy = 0
// Fill - wypełnienie wartością
span.Fill(42); // Wszystkie elementy = 42
// CopyTo - kopiowanie
int[] destination = new int[10];
span.CopyTo(destination);
// Slice method
Span<int> sliceMethod = span.Slice(2, 5); // [3, 4, 5]
Span<T> w foreach
// Span w foreach - bez alokacji!
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers.AsSpan();
// foreach przez Span - bez boxing, bez allocations
foreach (int num in span)
{
Console.WriteLine(num);
}
// Można też modyfikować przez ref
foreach (ref int num in span)
{
num *= 2; // Modyfikuje oryginalny array!
}
Console.WriteLine(string.Join(", ", numbers)); // 2, 4, 6, 8, 10
stackalloc i Span<T> - stack allocations
stackalloc przed C# 7.2 - unsafe!
// Przed C# 7.2 - stackalloc wymaga unsafe context
unsafe
{
int* buffer = stackalloc int[10]; // Stack allocation
// Użycie pointer arithmetic
for (int i = 0; i < 10; i++)
{
buffer[i] = i * 2;
}
}
// Problemy:
// 1. Wymaga unsafe context
// 2. Pointer arithmetic - error-prone
// 3. Brak bounds checking
// 4. Trudne w użyciu
stackalloc z Span<T> - safe i wygodne!
🎉 C# 7.2 - stackalloc z Span
Span<T> span = stackalloc T[size] - stack allocation BEZ unsafe!
// ✅ DOBRZE - małe buffery (< 1KB)
void ProcessData()
{
Span<byte> buffer = stackalloc byte[256]; // 256 bytes - OK
// Użyj buffer
FillBuffer(buffer);
}
// ✅ DOBRZE - temporary buffers
void ParseNumbers(string input)
{
Span<int> numbers = stackalloc int[10]; // Temporary
// Parse do buffer
int count = ParseIntoSpan(input, numbers);
}
// ⚠️ UWAŻAJ - duże buffery
void BadExample()
{
// Span<byte> huge = stackalloc byte[1024 * 1024]; // 1MB - StackOverflowException!
// Stack ma ~1MB - nie alokuj dużo na raz!
}
// Zasada: stackalloc dla bufferów < 1KB
// Dla większych - użyj array albo ArrayPool
stackalloc - praktyczne przykłady
// Przykład 1: String parsing bez alokacji
void ParseCoordinates(string input)
{
// input = "10,20,30,40"
Span<int> coords = stackalloc int[4]; // Stack buffer
int index = 0;
int current = 0;
foreach (char c in input)
{
if (c == ',')
{
coords[index++] = current;
current = 0;
}
else
{
current = current * 10 + (c - '0');
}
}
coords[index] = current;
// coords ma [10, 20, 30, 40] - zero heap allocations!
}
// Przykład 2: Byte manipulation
void ProcessBytes(byte[] data)
{
Span<byte> temp = stackalloc byte[16]; // 16-byte buffer
// Copy first 16 bytes
data.AsSpan(0, 16).CopyTo(temp);
// Process temp buffer
for (int i = 0; i < temp.Length; i++)
{
temp[i] ^= 0xFF; // XOR with 0xFF
}
// Copy back
temp.CopyTo(data.AsSpan(0, 16));
}
// Przykład 3: Hash computation
int ComputeSimpleHash(string text)
{
Span<int> hashes = stackalloc int[4]; // 4 hash buckets
foreach (char c in text)
{
int bucket = c % 4;
hashes[bucket] += c;
}
int result = 0;
foreach (int h in hashes)
{
result ^= h;
}
return result;
}
ReadOnlySpan<T> - immutable view
ReadOnlySpan<T> - read-only access
// ReadOnlySpan<T> - nie można modyfikować
int[] numbers = { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> span = numbers;
// Odczyt - OK
int first = span[0]; // 1
// Modyfikacja - BŁĄD!
// span[0] = 99; // ❌ BŁĄD kompilacji - readonly!
// Użycie: gdy nie chcesz pozwolić na modyfikację
void PrintNumbers(ReadOnlySpan<int> numbers)
{
foreach (int num in numbers)
{
Console.WriteLine(num);
}
// numbers[0] = 99; // ❌ BŁĄD - nie można modyfikować
}
ReadOnlySpan dla string - zero allocations!
// String jako ReadOnlySpan<char> - POTĘŻNE!
string text = "Hello World from C#";
// AsSpan - view na string bez alokacji
ReadOnlySpan<char> span = text.AsSpan();
// Slicing bez alokacji!
ReadOnlySpan<char> hello = span[0..5]; // "Hello" - BEZ alokacji!
ReadOnlySpan<char> world = span[6..11]; // "World" - BEZ alokacji!
// ToString() tylko gdy potrzebujesz string
string helloStr = hello.ToString(); // Teraz alokacja (gdy potrzeba)
// Porównanie bez alokacji
bool equals = hello.SequenceEqual("Hello"); // true - bez alokacji!
// StartsWith/EndsWith bez alokacji
bool starts = span.StartsWith("Hello"); // true
bool ends = span.EndsWith("C#"); // true
❌ String operacje - alokacje
string text = "Hello World from C#";
// Każda operacja = alokacja!
string hello = text.Substring(0, 5); // Alokacja
string world = text.Substring(6, 5); // Alokacja
bool starts = text.StartsWith("Hello"); // OK
bool equals = hello == "Hello"; // OK
// W pętli:
for (int i = 0; i < 100000; i++)
{
string sub = text.Substring(0, 5);
// 100k alokacji! 😱
}
✅ ReadOnlySpan - zero allocations
string text = "Hello World from C#";
ReadOnlySpan<char> span = text.AsSpan();
// Zero alokacji!
ReadOnlySpan<char> hello = span[0..5];
ReadOnlySpan<char> world = span[6..11];
bool starts = span.StartsWith("Hello");
bool equals = hello.SequenceEqual("Hello");
// W pętli:
for (int i = 0; i < 100000; i++)
{
ReadOnlySpan<char> sub = span[0..5];
// Zero alokacji! ✨
}
// C# 14 - możesz mieć overloads dla array i Span
class DataProcessor
{
// Stary API - backward compatibility
public void Process(int[] data)
{
Console.WriteLine("Array overload");
Process(data.AsSpan()); // Deleguj do Span version
}
// Nowy API - zero allocations
public void Process(Span<int> data)
{
Console.WriteLine("Span overload");
foreach (ref int num in data)
{
num *= 2;
}
}
}
var processor = new DataProcessor();
int[] array = { 1, 2, 3 };
// C# 14 wywołuje Span overload dzięki implicit conversion!
processor.Process(array); // "Span overload" ✨
Memory<T> - heap-safe alternative
Problem ze Span<T> - tylko stack
// Span<T> ma ograniczenie - ref struct
// NIE może być:
// 1. Polem w klasie
// 2. Używany w async/await
// 3. Stored w heap
// ❌ To NIE działa
class Container
{
// private Span<int> _data; // ❌ BŁĄD - Span nie może być polem!
}
// ❌ To też NIE działa
async Task ProcessAsync(Span<int> data) // ❌ BŁĄD - Span w async!
{
await Task.Delay(100);
// ...
}
Memory<T> - rozwiązanie!
🎉 C# 7.2 - Memory<T>
Memory<T> to heap-safe wrapper wokół Span<T> - może być przechowywany, async-friendly!
// Memory<T> - może być wszędzie!
class Container
{
private Memory<int> _data; // ✅ OK - Memory może być polem!
public Container(int[] array)
{
_data = array; // Implicit conversion
}
public void Process()
{
Span<int> span = _data.Span; // Get Span when needed
foreach (ref int num in span)
{
num *= 2;
}
}
}
// ✅ Memory w async - działa!
async Task ProcessAsync(Memory<int> data)
{
await Task.Delay(100);
// Get Span when needed
Span<int> span = data.Span;
foreach (ref int num in span)
{
num *= 2;
}
}
Memory vs Span - kiedy czego używać?
Feature
Span<T>
Memory<T>
Gdzie można użyć
Stack only (local variables)
Stack + Heap (fields, async)
Performance
Fastest - no overhead
Tiny overhead
Może być polem w klasie?
❌ Nie
✅ Tak
Async/await?
❌ Nie
✅ Tak
Get Span
-
.Span property
Kiedy używać
Synchroniczne, lokalne operacje
Async, storing, passing around
// Zasada: Span w metodach, Memory w klasach/async
class DataBuffer
{
private Memory<byte> _buffer; // Memory - może być polem
public DataBuffer(int size)
{
_buffer = new byte[size];
}
// Synchroniczna metoda - używa Span
public void Process()
{
Span<byte> span = _buffer.Span;
// Fast processing z Span
}
// Async metoda - używa Memory
public async Task ProcessAsync()
{
await Task.Delay(100);
Span<byte> span = _buffer.Span; // Get Span when needed
// Processing
}
}
String Parsing z Span - praktyczne przykłady
CSV parsing - zero allocations
// CSV parsing bez alokacji!
void ParseCSVLine(ReadOnlySpan<char> line, Span<int> output)
{
int outputIndex = 0;
int current = 0;
bool inNumber = false;
foreach (char c in line)
{
if (c >= '0' && c <= '9')
{
current = current * 10 + (c - '0');
inNumber = true;
}
else if (c == ',' && inNumber)
{
output[outputIndex++] = current;
current = 0;
inNumber = false;
}
}
if (inNumber)
{
output[outputIndex] = current;
}
}
// Użycie
string csv = "10,20,30,40,50";
Span<int> numbers = stackalloc int[5]; // Stack buffer
ParseCSVLine(csv.AsSpan(), numbers); // Zero heap allocations!
foreach (int num in numbers)
{
Console.WriteLine(num); // 10, 20, 30, 40, 50
}
String splitting z Span
// String splitting bez alokacji
void ProcessWords(ReadOnlySpan<char> text)
{
Span<Range> ranges = stackalloc Range[10]; // Max 10 words
int count = text.Split(ranges, ' ');
for (int i = 0; i < count; i++)
{
ReadOnlySpan<char> word = text[ranges[i]];
// Process word bez alokacji
Console.WriteLine(word.ToString());
}
}
string sentence = "Hello World from C# Span";
ProcessWords(sentence); // Zero allocations dla splitting!
Number parsing z Span
// Number parsing z Span - zero allocations
bool TryParseInt(ReadOnlySpan<char> text, out int result)
{
result = 0;
bool negative = false;
int start = 0;
// Check for negative
if (text.Length > 0 && text[0] == '-')
{
negative = true;
start = 1;
}
for (int i = start; i < text.Length; i++)
{
char c = text[i];
if (c < '0' || c > '9')
return false;
result = result * 10 + (c - '0');
}
if (negative)
result = -result;
return true;
}
// Użycie
string input = "12345";
ReadOnlySpan<char> span = input.AsSpan();
if (TryParseInt(span, out int number))
{
Console.WriteLine(number); // 12345 - zero allocations!
}
// .NET też ma Span-based parsing!
if (int.TryParse(span, out int num2))
{
Console.WriteLine(num2); // Built-in Span support!
}
Path parsing z Span
// Path parsing - zero allocations
void ParsePath(ReadOnlySpan<char> path)
{
// Find last slash
int lastSlash = path.LastIndexOf('/');
if (lastSlash == -1)
lastSlash = path.LastIndexOf('\\');
if (lastSlash != -1)
{
ReadOnlySpan<char> directory = path[..lastSlash];
ReadOnlySpan<char> filename = path[(lastSlash + 1)..];
Console.WriteLine($"Dir: {directory.ToString()}");
Console.WriteLine($"File: {filename.ToString()}");
// Find extension
int lastDot = filename.LastIndexOf('.');
if (lastDot != -1)
{
ReadOnlySpan<char> name = filename[..lastDot];
ReadOnlySpan<char> ext = filename[(lastDot + 1)..];
Console.WriteLine($"Name: {name.ToString()}");
Console.WriteLine($"Ext: {ext.ToString()}");
}
}
}
string path = "/home/user/documents/file.txt";
ParsePath(path);
// Dir: /home/user/documents
// File: file.txt
// Name: file
// Ext: txt
// Zero allocations podczas parsowania!
Practical Use Cases - kiedy używać Span
Use Case 1: High-performance parsing
// JSON parsing z Span (conceptual)
class JsonParser
{
public void ParseObject(ReadOnlySpan<char> json)
{
Span<Range> properties = stackalloc Range[20];
// Parse properties bez alokacji
int count = ExtractProperties(json, properties);
for (int i = 0; i < count; i++)
{
ReadOnlySpan<char> prop = json[properties[i]];
ProcessProperty(prop);
}
}
}
Use Case 2: Cryptography i hashing
// SHA256 z Span - zero allocations
void ComputeHash(ReadOnlySpan<byte> data, Span<byte> hash)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
// Compute hash bez alokacji
sha256.TryComputeHash(data, hash, out _);
}
// Użycie
byte[] data = new byte[1024];
Span<byte> hash = stackalloc byte[32]; // SHA256 = 32 bytes
ComputeHash(data, hash); // Zero allocations!
Use Case 3: Binary protocol parsing
// Binary protocol - zero allocations
readonly struct PacketHeader
{
public byte Version { get; init; }
public byte Type { get; init; }
public ushort Length { get; init; }
}
PacketHeader ParseHeader(ReadOnlySpan<byte> data)
{
return new PacketHeader
{
Version = data[0],
Type = data[1],
Length = (ushort)(data[2] | (data[3] << 8))
};
}
// Network packet processing
void ProcessPacket(ReadOnlySpan<byte> packet)
{
var header = ParseHeader(packet[..4]);
ReadOnlySpan<byte> payload = packet[4..(4 + header.Length)];
// Process payload bez alokacji
ProcessPayload(header.Type, payload);
}
Use Case 4: Image processing
// Image processing z Span
void FlipImage(Span<byte> pixels, int width, int height)
{
int bytesPerRow = width * 4; // RGBA = 4 bytes per pixel
Span<byte> temp = stackalloc byte[bytesPerRow];
for (int y = 0; y < height / 2; y++)
{
int topRow = y * bytesPerRow;
int bottomRow = (height - 1 - y) * bytesPerRow;
Span<byte> top = pixels.Slice(topRow, bytesPerRow);
Span<byte> bottom = pixels.Slice(bottomRow, bytesPerRow);
// Swap rows bez alokacji
top.CopyTo(temp);
bottom.CopyTo(top);
temp.CopyTo(bottom);
}
}