Created
December 1, 2025 05:21
-
-
Save devops-school/deb3eba93300e352e9a464a84f7de072 to your computer and use it in GitHub Desktop.
DOTNET: Memory Optimization in .NET with Span
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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