Exercises: Span<T> and Memory<T> (Day 5-6)
Exercise 1: Zero-Allocation String Parsing
Parse delimited strings using Span<T> without creating intermediate string allocations.
Requirements
public class SpanStringParser
{
// TODO: Parse CSV line into fields using Span
public ReadOnlySpan\<char\> GetField(ReadOnlySpan\<char\> csvLine, int fieldIndex)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Parse key=value pairs
public bool TryParseKeyValue(
ReadOnlySpan\<char\> input,
out ReadOnlySpan\<char\> key,
out ReadOnlySpan\<char\> value)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Split by delimiter without allocations
public int SplitInto(
ReadOnlySpan\<char\> input,
char delimiter,
Span<ReadOnlySpan\<char\>> results)
{
// Your implementation here
throw new NotImplementedException();
}
}
Expected Behavior
var parser = new SpanStringParser();
// Parse CSV
string csvLine = "John,Doe,30,Engineer";
var firstName = parser.GetField(csvLine, 0);
Console.WriteLine(firstName.ToString()); // "John"
// Parse key-value
string config = "timeout=30";
if (parser.TryParseKeyValue(config, out var key, out var value))
{
Console.WriteLine($"{key.ToString()}={value.ToString()}"); // timeout=30
}
// Split
string data = "apple|banana|cherry";
Span<ReadOnlySpan\<char\>> fields = stackalloc ReadOnlySpan\<char\>[10];
int count = parser.SplitInto(data, '|', fields);
// count = 3, no string allocations!
Hints
- Use
Span.IndexOf()to find delimiters - Use
Span.Slice()to extract portions - Don't call
ToString()until absolutely necessary - Return
ReadOnlySpan\<char\>to prevent modifications - Use
stackallocfor temporary storage
Exercise 2: High-Performance Number Parsing
Parse integers and decimals from spans without string allocation.
Requirements
public class SpanNumberParser
{
// TODO: Parse integer from span
public static int ParseInt(ReadOnlySpan\<char\> input)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Try parse with error handling
public static bool TryParseInt(ReadOnlySpan\<char\> input, out int result)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Parse multiple numbers from delimited string
public static void ParseIntegers(
ReadOnlySpan\<char\> input,
char delimiter,
Span<int> results,
out int count)
{
// Your implementation here
throw new NotImplementedException();
}
}
Expected Behavior
// Parse from substring without allocation
string input = "Value: 12345";
int value = SpanNumberParser.ParseInt(input.AsSpan(7));
Console.WriteLine(value); // 12345
// Try parse
if (SpanNumberParser.TryParseInt("42".AsSpan(), out int result))
{
Console.WriteLine(result); // 42
}
// Parse multiple
string numbers = "10,20,30,40,50";
Span<int> results = stackalloc int[10];
SpanNumberParser.ParseIntegers(numbers, ',', results, out int count);
// results contains [10, 20, 30, 40, 50], count = 5
Hints
- Use
int.Parse(ReadOnlySpan\<char\>)- it's allocation-free! - Use
int.TryParse(ReadOnlySpan\<char\>, out int)for safe parsing - Split input into spans for each number
- No need to create string intermediates
Exercise 3: ArrayPool Buffer Management
Implement a buffer pool-based file processor.
Requirements
public class PooledFileProcessor
{
private const int BufferSize = 4096;
// TODO: Process large file using ArrayPool
public async Task<int> CountLinesAsync(string filePath)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Copy file using pooled buffers
public async Task CopyFileAsync(string source, string destination)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Process chunks with callback
public async Task ProcessChunksAsync(
string filePath,
Func<ReadOnlyMemory<byte>, Task> processor)
{
// Your implementation here
throw new NotImplementedException();
}
}
Expected Behavior
var processor = new PooledFileProcessor();
// Count lines without loading entire file
int lineCount = await processor.CountLinesAsync("large.txt");
Console.WriteLine($"Lines: {lineCount}");
// Copy file efficiently
await processor.CopyFileAsync("source.bin", "dest.bin");
// Process in chunks
await processor.ProcessChunksAsync("data.bin", async chunk =>
{
// Process each chunk
Console.WriteLine($"Processing {chunk.Length} bytes");
});
// All using ArrayPool - minimal GC pressure!
Hints
- Rent buffer with
ArrayPool<byte>.Shared.Rent(size) - Always return in
finallyblock - Use
Memory<byte>for async operations - Read file in chunks
- Count newlines in each chunk
Exercise 4: Stack-Allocated Buffers
Use stackalloc for small temporary buffers.
Requirements
public class StackBufferExamples
{
// TODO: Format message into stack buffer
public static ReadOnlySpan\<char\> FormatMessage(int id, ReadOnlySpan\<char\> text)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Convert number to hex without allocation
public static void ToHexString(int value, Span\<char\> output)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Reverse string in place
public static void ReverseInPlace(Span\<char\> text)
{
// Your implementation here
throw new NotImplementedException();
}
}
Expected Behavior
// Format message
Span\<char\> buffer = stackalloc char[128];
var message = StackBufferExamples.FormatMessage(123, "Hello");
Console.WriteLine(message.ToString()); // "ID:123 Hello"
// To hex
Span\<char\> hex = stackalloc char[8];
StackBufferExamples.ToHexString(255, hex);
Console.WriteLine(hex.ToString()); // "000000FF"
// Reverse
Span\<char\> text = stackalloc char[] { 'H', 'e', 'l', 'l', 'o' };
StackBufferExamples.ReverseInPlace(text);
Console.WriteLine(text.ToString()); // "olleH"
Hints
- Use
stackallocfor buffers < 1KB - Use
Span\<T\>methods likeCopyTo(),Fill() - Use
value.TryFormat()for formatting numbers - Reverse by swapping elements from both ends
- Don't return stack-allocated memory!
Exercise 5: Memory<T> Async Operations
Work with Memory<T> in async methods where Span<T> can't be used.
Requirements
public class MemoryAsyncProcessor
{
// TODO: Process data asynchronously
public async Task<int> ProcessDataAsync(Memory<byte> data)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Read file into memory buffer
public async Task<Memory<byte>> ReadFileAsync(string filePath)
{
// Your implementation here
throw new NotImplementedException();
}
// TODO: Pipeline processing
public async Task PipelineAsync(
Memory<byte> input,
Func<ReadOnlyMemory<byte>, Task<Memory<byte>>> stage1,
Func<ReadOnlyMemory<byte>, Task<Memory<byte>>> stage2)
{
// Your implementation here
throw new NotImplementedException();
}
}
Expected Behavior
var processor = new MemoryAsyncProcessor();
// Process with Memory (works in async)
Memory<byte> data = new byte[1024];
await FillWithDataAsync(data);
int result = await processor.ProcessDataAsync(data);
// Read file
Memory<byte> fileData = await processor.ReadFileAsync("data.bin");
// Pipeline
await processor.PipelineAsync(
data,
async chunk => { /* transform */ return chunk; },
async chunk => { /* transform */ return chunk; }
);
Hints
- Use
Memory\<T\>in async method signatures - Convert to
Span\<T\>when needed:memory.Span - Use
ReadOnlyMemory\<T\>for read-only data - Pass Memory to async I/O operations
- Use ArrayPool for the underlying buffer
Exercise 6: Custom Span Operations
Create extension methods for common Span operations.
Requirements
public static class SpanExtensions
{
// TODO: Count occurrences
public static int Count\<T\>(this ReadOnlySpan\<T\> span, T value)
where T : IEquatable\<T\>
{
throw new NotImplementedException();
}
// TODO: Index of nth occurrence
public static int IndexOfNth\<T\>(this ReadOnlySpan\<T\> span, T value, int occurrence)
where T : IEquatable\<T\>
{
throw new NotImplementedException();
}
// TODO: Trim characters from both ends
public static ReadOnlySpan\<char\> Trim(
this ReadOnlySpan\<char\> span,
ReadOnlySpan\<char\> trimChars)
{
throw new NotImplementedException();
}
// TODO: Replace all occurrences
public static void ReplaceAll\<T\>(this Span\<T\> span, T oldValue, T newValue)
where T : IEquatable\<T\>
{
throw new NotImplementedException();
}
}
Expected Behavior
// Count
ReadOnlySpan<int> numbers = stackalloc int[] { 1, 2, 3, 2, 1 };
int count = numbers.Count(2); // 2
// Index of nth
ReadOnlySpan\<char\> text = "banana";
int index = text.IndexOfNth('a', 2); // 3 (second 'a')
// Trim
ReadOnlySpan\<char\> padded = " hello ";
var trimmed = padded.Trim(" ".AsSpan());
Console.WriteLine(trimmed.ToString()); // "hello"
// Replace all
Span<int> data = stackalloc int[] { 1, 2, 3, 2, 1 };
data.ReplaceAll(2, 5);
// data is now { 1, 5, 3, 5, 1 }
Hints
- Use
foreachor manual indexing to iterate - Use
IEquatable\<T\>.Equals()for comparisons - Return sliced spans for Trim
- Modify in-place for ReplaceAll
- Keep it allocation-free!
Exercise 7: Binary Protocol Parser
Parse binary protocol messages using Span<T>.
Requirements
public readonly ref struct BinaryMessageParser
{
private readonly ReadOnlySpan<byte> _data;
public BinaryMessageParser(ReadOnlySpan<byte> data)
{
_data = data;
}
// TODO: Parse header
public MessageHeader GetHeader()
{
throw new NotImplementedException();
}
// TODO: Get payload
public ReadOnlySpan<byte> GetPayload()
{
throw new NotImplementedException();
}
// TODO: Validate checksum
public bool IsValid()
{
throw new NotImplementedException();
}
}
public struct MessageHeader
{
public byte Version { get; set; }
public ushort MessageType { get; set; }
public int Length { get; set; }
}
Message Format
[Version:1][Type:2][Length:4][Payload:Length][Checksum:2]
Expected Behavior
byte[] message = CreateMessage();
var parser = new BinaryMessageParser(message);
var header = parser.GetHeader();
Console.WriteLine($"Version: {header.Version}");
Console.WriteLine($"Type: {header.MessageType}");
Console.WriteLine($"Length: {header.Length}");
var payload = parser.GetPayload();
if (parser.IsValid())
{
ProcessPayload(payload);
}
Hints
- Use
BitConverter.ToInt32()etc. for parsing - Use
_data.Slice()to extract sections - Calculate checksum from payload
- Make it a
ref structso it can hold Span - Zero allocations!
Exercise 8: Performance Benchmark
Compare performance of traditional vs Span-based approaches.
Requirements
public class SpanBenchmark
{
private const string SampleData = "field1,field2,field3,field4,field5";
[Benchmark]
public string[] Traditional_Split()
{
// TODO: Use string.Split
throw new NotImplementedException();
}
[Benchmark]
public void Span_Split()
{
// TODO: Use Span-based splitting
throw new NotImplementedException();
}
[Benchmark]
public string Traditional_Substring()
{
// TODO: Use string.Substring
throw new NotImplementedException();
}
[Benchmark]
public ReadOnlySpan\<char\> Span_Slice()
{
// TODO: Use Span.Slice
throw new NotImplementedException();
}
}
Expected Results
| Method | Mean | Allocated |
|-------------------- |----------:|----------:|
| Traditional_Split | 250.0 ns | 384 B |
| Span_Split | 50.0 ns | 0 B |
| Traditional_Substring| 100.0 ns | 64 B |
| Span_Slice | 5.0 ns | 0 B |
Hints
- Use BenchmarkDotNet
- Measure both time and allocations
- Include
[MemoryDiagnoser]attribute - Show dramatic allocation reduction with Span
- Don't call
ToString()in Span benchmark!
Bonus Exercise: Span-Based StringBuilder
Create a stack-allocated string builder using Span<T>.
Requirements
public ref struct SpanStringBuilder
{
private Span\<char\> _buffer;
private int _position;
public SpanStringBuilder(Span\<char\> buffer)
{
_buffer = buffer;
_position = 0;
}
// TODO: Append string
public void Append(ReadOnlySpan\<char\> text) { }
// TODO: Append number
public void Append(int value) { }
// TODO: Append with format
public void AppendFormat(int value, string format) { }
// TODO: Get result
public ReadOnlySpan\<char\> AsSpan() => _buffer.Slice(0, _position);
}
Expected Behavior
Span\<char\> buffer = stackalloc char[128];
var builder = new SpanStringBuilder(buffer);
builder.Append("User ID: ");
builder.Append(12345);
builder.Append(", Status: ");
builder.Append("Active");
string result = builder.AsSpan().ToString();
Console.WriteLine(result); // "User ID: 12345, Status: Active"
Validation Tests
[Fact]
public void ParseField_ReturnsCorrectValue()
{
var parser = new SpanStringParser();
var field = parser.GetField("a,b,c", 1);
Assert.Equal("b", field.ToString());
}
[Fact]
public void StackBuffer_NoAllocation()
{
var before = GC.GetAllocatedBytesForCurrentThread();
Span\<char\> buffer = stackalloc char[128];
buffer[0] = 'H';
var after = GC.GetAllocatedBytesForCurrentThread();
Assert.Equal(before, after); // No heap allocation!
}
[Fact]
public async Task ArrayPool_ReducesGCPressure()
{
long allocated = 0;
for (int i = 0; i < 1000; i++)
{
var before = GC.GetAllocatedBytesForCurrentThread();
var buffer = ArrayPool<byte>.Shared.Rent(4096);
ArrayPool<byte>.Shared.Return(buffer);
var after = GC.GetAllocatedBytesForCurrentThread();
allocated += (after - before);
}
// Should be minimal
Assert.True(allocated < 10_000);
}
[Fact]
public void SpanCount_WorksCorrectly()
{
ReadOnlySpan<int> numbers = stackalloc int[] { 1, 2, 3, 2, 1 };
Assert.Equal(2, numbers.Count(2));
Assert.Equal(2, numbers.Count(1));
Assert.Equal(1, numbers.Count(3));
}
Learning Objectives
By completing these exercises, you should be able to:
✅ Parse strings with Span<T> without allocations ✅ Use ArrayPool for efficient buffer management ✅ Leverage stackalloc for small temporary buffers ✅ Work with Memory<T> in async scenarios ✅ Create custom Span-based operations ✅ Parse binary data with zero allocations ✅ Benchmark and measure allocation improvements ✅ Build high-performance parsers and formatters
Next Steps
After completing these exercises:
- Profile with BenchmarkDotNet
- Measure allocation improvements
- Test with large datasets
- Compare Span vs traditional approaches
- Move to Day 7 exercises on Async Streams