Skip to main content

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 stackalloc for 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 finally block
  • 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 stackalloc for buffers < 1KB
  • Use Span\<T\> methods like CopyTo(), 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 foreach or 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 struct so 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:

  1. Profile with BenchmarkDotNet
  2. Measure allocation improvements
  3. Test with large datasets
  4. Compare Span vs traditional approaches
  5. Move to Day 7 exercises on Async Streams