Skip to content

Instantly share code, notes, and snippets.

@devops-school
Created December 1, 2025 05:21
Show Gist options
  • Select an option

  • Save devops-school/deb3eba93300e352e9a464a84f7de072 to your computer and use it in GitHub Desktop.

Select an option

Save devops-school/deb3eba93300e352e9a464a84f7de072 to your computer and use it in GitHub Desktop.
DOTNET: Memory Optimization in .NET with Span
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
const int itemCount = 500_000; // number of strings to process
Console.WriteLine("=========================================");
Console.WriteLine(" Span<T> Demo – Substring vs Span ");
Console.WriteLine("=========================================\n");
Console.WriteLine($"Items to process: {itemCount:N0}\n");
// 1. Prepare test data (shared by both scenarios)
Console.WriteLine("Preparing test data...");
string[] data = BuildTestData(itemCount);
// 2. Warmup to remove JIT noise
Console.WriteLine("Warming up (small runs)...");
RunScenario("Warmup – Substring (no Span)", data, RunWithSubstring, iterationsFraction: 0.1);
RunScenario("Warmup – Span<T>", data, RunWithSpan, iterationsFraction: 0.1);
Console.WriteLine();
Console.WriteLine("=========== REAL TESTS (Release) ==========\n");
// 3. Real scenarios
RunScenario("WITHOUT Span<T> – using Substring (allocations)", data, RunWithSubstring);
RunScenario("WITH Span<T> – using AsSpan + Slice (no extra allocations)", data, RunWithSpan);
Console.WriteLine("Done. Press any key to exit...");
Console.ReadKey();
}
// Build an array of strings we will parse in both scenarios.
private static string[] BuildTestData(int count)
{
var result = new string[count];
for (int i = 0; i < count; i++)
{
// Example format: "00001234-ABC-XYZ-1234567890"
// - 8 digits
// - "-ABC-"
// - "XYZ"
// - "-"
// - 10-digit number
result[i] = $"{i:D8}-ABC-XYZ-1234567890";
}
return result;
}
/// <summary>
/// Generic scenario runner that captures time + GC counts.
/// </summary>
private static void RunScenario(
string name,
string[] data,
Func<string[], int, long> worker,
double iterationsFraction = 1.0)
{
int iterations = (int)(data.Length * iterationsFraction);
if (iterations <= 0)
iterations = 1;
// Force a GC before each scenario for a cleaner baseline
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
long gen0Before = GC.CollectionCount(0);
long gen1Before = GC.CollectionCount(1);
long gen2Before = GC.CollectionCount(2);
long startMemory = GC.GetTotalMemory(forceFullCollection: true);
var sw = Stopwatch.StartNew();
long checksum = worker(data, iterations); // run the scenario
sw.Stop();
long endMemory = GC.GetTotalMemory(forceFullCollection: true);
long gen0After = GC.CollectionCount(0);
long gen1After = GC.CollectionCount(1);
long gen2After = GC.CollectionCount(2);
Console.WriteLine($"--- {name} ---");
Console.WriteLine($"Items processed : {iterations:N0}");
Console.WriteLine($"Time Elapsed : {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"GC Gen0 : {gen0After - gen0Before}");
Console.WriteLine($"GC Gen1 : {gen1After - gen1Before}");
Console.WriteLine($"GC Gen2 : {gen2After - gen2Before}");
long diffBytes = endMemory - startMemory;
Console.WriteLine($"Managed Memory Δ: {diffBytes / 1024.0 / 1024.0:F2} MB");
Console.WriteLine($"Checksum (ignore, just prevents JIT from optimizing away work): {checksum}");
Console.WriteLine();
}
/// <summary>
/// Scenario WITHOUT Span<T> – uses Substring, which allocates new strings.
/// </summary>
private static long RunWithSubstring(string[] data, int iterations)
{
long sum = 0;
for (int i = 0; i < iterations; i++)
{
string s = data[i];
// These all allocate NEW string instances:
string part1 = s.Substring(0, 8); // first 8 chars (digits)
string part2 = s.Substring(9, 3); // "ABC"
string part3 = s.Substring(s.Length - 10); // last 10 chars
// Pretend to "use" the parts – here we just sum lengths & char codes
sum += part1.Length + part2.Length + part3.Length;
if (part1[0] == '0') sum++;
if (part2 == "ABC") sum++;
if (part3.EndsWith("90")) sum++;
}
return sum;
}
/// <summary>
/// Scenario WITH Span<T> – uses AsSpan + Slice, NO extra strings allocated.
/// </summary>
private static long RunWithSpan(string[] data, int iterations)
{
long sum = 0;
for (int i = 0; i < iterations; i++)
{
string s = data[i];
ReadOnlySpan<char> span = s.AsSpan();
// All of these are just views over the original string – NO allocations:
var part1 = span.Slice(0, 8); // first 8 chars
var part2 = span.Slice(9, 3); // "ABC"
var part3 = span.Slice(span.Length - 10); // last 10 chars
sum += part1.Length + part2.Length + part3.Length;
if (part1[0] == '0') sum++;
if (part2.SequenceEqual("ABC".AsSpan())) sum++;
if (part3.EndsWith("90".AsSpan(), StringComparison.Ordinal)) sum++;
}
return sum;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment