🚀 Zero-Allocation String Operations
Substring creates new string. Array slicing copies memory. Span<T> provides zero-copy views. Massive performance gains.
The Problem with Substring
// ❌ Traditional: Allocates new strings string data = "2024-03-19T10:30:00"; string year = data.Substring(0, 4); // Allocates "2024" string month = data.Substring(5, 2); // Allocates "03" string day = data.Substring(8, 2); // Allocates "19" // Process 1 million dates: 3 million allocations! // Garbage collector works overtime // ✅ Span: Zero allocations ReadOnlySpandata = "2024-03-19T10:30:00"; ReadOnlySpan year = data.Slice(0, 4); // No allocation ReadOnlySpan month = data.Slice(5, 2); // No allocation ReadOnlySpan day = data.Slice(8, 2); // No allocation int yearNum = int.Parse(year); // Parses directly from span
🎯 Span Basics
// Stack-allocated array (no heap allocation!) Spannumbers = stackalloc int[100]; numbers[0] = 42; // View into existing array (no copy!) int[] array = new int[100]; Span slice = array.AsSpan(10, 50); // Elements 10-59 slice[0] = 100; // Modifies original array // String as ReadOnlySpan (immutable) ReadOnlySpan text = "Hello, World!"; ReadOnlySpan hello = text.Slice(0, 5); // Can't do this (compile error): // Span span = "Hello"; // Strings are immutable
Real-World: CSV Parsing
// ❌ Traditional: Many allocations public ListParseCSV(string csv) { var people = new List (); var lines = csv.Split('\n'); // Allocates array of strings foreach (var line in lines) { var parts = line.Split(','); // Allocates array per line people.Add(new Person { Name = parts[0], // Already allocated Age = int.Parse(parts[1]) }); } return people; } // ✅ Span: Minimal allocations public List ParseCSVFast(ReadOnlySpan csv) { var people = new List (); while (csv.Length > 0) { int newline = csv.IndexOf('\n'); var line = newline >= 0 ? csv.Slice(0, newline) : csv; csv = newline >= 0 ? csv.Slice(newline + 1) : default; int comma = line.IndexOf(','); var name = line.Slice(0, comma); var age = line.Slice(comma + 1); people.Add(new Person { Name = name.ToString(), // Only allocation here Age = int.Parse(age) }); } return people; } // Benchmark: 10x faster, 90% less memory
stackalloc for Temp Buffers
// ❌ Allocates array on heap byte[] buffer = new byte[256]; ReadData(buffer); // ✅ Stack-allocated (no GC pressure) Spanbuffer = stackalloc byte[256]; ReadData(buffer); // Warning: Don't stackalloc large arrays! // Stack is limited (typically 1MB) // Use for small buffers only (< 1KB) // Safe pattern Span buffer = size <= 1024 ? stackalloc byte[size] : new byte[size];
🔧 Memory<T> for Async
// Span can't be used in async methods (stack-based) // Use Memoryinstead public async Task ProcessAsync(Memory data) { // Can hold across await await Task.Delay(100); // Convert to Span when needed Span span = data.Span; span[0] = 42; } // ReadOnlyMemory for immutable public async Task CountAsync(ReadOnlyMemory text) { await Task.Delay(100); return text.Span.Count('a'); }
✅ When to Use Span
- String parsing: CSV, JSON, log files
- Binary protocols: Network packets, file formats
- Hot paths: Performance-critical loops
- Large data processing: Avoid allocations
- Temporary buffers: stackalloc for small sizes
⚠️ Limitations
- Can't store in class fields: Span is stack-only (ref struct)
- Can't use in async: Use Memory<T> instead
- Can't box: No object, IEnumerable, etc.
- Learning curve: Different mental model
"Log parser was allocating 500MB/second. Rewrote with Span<char>. Allocations down to 5MB/second. GC pauses gone. Throughput tripled. Span is game-changing for high-performance .NET."
