BenchmarkDotNet With .NET 10 Perf Optimization - Advanced Performance Engineering Guide - Part 2
Benchmark Pipeline, Performance Budget, Regression Gate, Canary Benchmark, Baseline Comparison, Threshold Validation, Historical Trending…
BenchmarkDotNet With .NET 10 Perf Optimization - Advanced Performance Engineering Guide - Part 2
Benchmark Pipeline, Performance Budget, Regression Gate, Canary Benchmark, Baseline Comparison, Threshold Validation, Historical Trending, Multi-runtime Validation.

📖 Introduction
In **BenchmarkDotNet With .NET 10 Perf Optimization — Foundations & Methodology for C# Devs — Part 1**, we established the foundation of evidence-based optimization with BenchmarkDotNet on .NET 10. We covered installation, basic attributes, SOLID-compliant benchmark patterns, and implemented benchmarks across five critical Vehixcare components. We saw how simple changes like switching to MessagePack serialization (3.6x faster) or implementing bulk MongoDB writes (14.3x faster) could transform system performance.
But measurement is only half the battle.
Part 2 dives into the advanced techniques that separate novice optimizers from performance engineers. We’ll explore what happens inside the CPU, how to catch regressions before they reach production, and how to build automated performance gates that fail CI/CD pipelines when benchmarks regress.
📚 Key Takeaways from Foundations & Methodology (Part 1)
Before proceeding: BenchmarkDotNet fundamentals (warmup, outlier removal, statistical confidence), .NET 10 advantages (AVX-512 512-bit SIMD, Dynamic PGO, NativeAOT, NUMA-aware GC), Vehixcare performance baselines (1,021 ns deserialization, 8,234 ns scoring, 5s DB writes), quick wins achieved (MessagePack 3.6x faster, bulk MongoDB 14.3x faster, SignalR grouping 55x faster), SOLID-compliant benchmark patterns, and optimization priority matrix (P0 quick wins vs P2 strategic) — tools now in hand.
🔍 What’s in This Story (Advanced Performance Engineering Guide)
Deep dive into advanced memory diagnostics (GC generations, pinned objects, native memory, allocation profiling, Large Object Heap analysis), hardware counter deep dive (cache misses, branch mispredictions, instruction retirement, CPU pipeline analysis, TLB misses), cross-runtime regression testing (.NET 8/9/10 comparison, automated performance gates), production performance monitoring with OpenTelemetry integration, real-world Vehixcare optimization case studies (telemetry deserialization 10.4x, geo-fencing 13.2x, MongoDB writes 14.3x), custom benchmarking attributes for domain-specific needs, continuous performance testing with historical trending, and CI/CD integration with regression detection.
Patterns covered in this story: Benchmark Pipeline, Performance Budget, Regression Gate, Canary Benchmark, Baseline Comparison, Threshold Validation, Historical Trending, Multi-runtime Validation.
📖 Complete Series Navigation
• BenchmarkDotNet With .NET 10 Perf Optimization — Foundations & Methodology for C# Devs — Part 1 ✅ Published
• BenchmarkDotNet With .NET 10 Perf Optimization — Advanced Performance Engineering Guide — Part 2 ✅ Published (you are here)
• BenchmarkDotNet With .NET 10 Perf Optimization — AI-Powered Performance Engineering — Part 3 — Comming soon
• BenchmarkDotNet With .NET 10 Perf Optimization — The Future of Performance Tuning — Part 4 ✅ Published
1.0 Advanced Memory Diagnostics
1.1 Understanding .NET 10 GC Generations
The .NET Garbage Collector is generation-based, optimizing for the fact that most objects die young. Understanding GC behavior is critical for high-performance systems like Vehixcare.

1.2 GC Generation Benchmarking
// Vehixcare.Performance.Benchmarks/Memory/GCGenerationBenchmarks.cs
// SOLID: Single Responsibility - Each benchmark tests specific GC behavior
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System.Runtime.InteropServices;
namespace Vehixcare.Performance.Benchmarks.Memory;
[SimpleJob(RuntimeMoniker.Net100)]
[MemoryDiagnoser(true)] // .NET 10: Enhanced GC tracking with pinned object detection
[GCForce(true)] // Forces GC between iterations for consistent results
public class GCGenerationBenchmarks
{
private List<byte[]> _largeObjects = new();
private List<SmallObject> _smallObjects = new();
private GCHandle _pinnedHandle;
[Params(1000, 10000, 100000)]
public int ObjectCount { get; set; }
[GlobalSetup]
public void Setup()
{
// Create objects of different sizes to target different generations
_smallObjects = Enumerable.Range(0, ObjectCount)
.Select(i => new SmallObject { Id = i, Data = new byte[32] }) // 32 bytes - Gen0
.ToList();
_largeObjects = Enumerable.Range(0, ObjectCount / 10)
.Select(i => new byte[90_000]) // >85KB - LOH
.ToList();
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Short-lived objects - Gen0 collection")]
public int CreateShortLivedObjects()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
// These objects die immediately - Gen0 only
var obj = new SmallObject { Id = i, Data = new byte[32] };
sum += obj.Id;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Long-lived objects - Promoted to Gen2")]
public int CreateLongLivedObjects()
{
var cache = new List<SmallObject>();
int sum = 0;
for (int i = 0; i < 10000; i++)
{
var obj = new SmallObject { Id = i, Data = new byte[32] };
cache.Add(obj); // Object survives - promoted to Gen1, then Gen2
sum += obj.Id;
}
// Keep reference to prevent collection
GC.KeepAlive(cache);
return sum;
}
[Benchmark]
[BenchmarkDescription("Large Object Heap allocation >85KB")]
public int AllocateLargeObjects()
{
int sum = 0;
for (int i = 0; i < 100; i++)
{
// LOH allocation - expensive, rarely collected
var largeArray = new byte[90_000];
sum += largeArray.Length;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("ArrayPool - Avoid LOH allocation")]
public int AllocateWithArrayPool()
{
var pool = ArrayPool<byte>.Shared;
int sum = 0;
for (int i = 0; i < 100; i++)
{
var buffer = pool.Rent(90_000); // Reuses from pool - no LOH allocation
sum += buffer.Length;
pool.Return(buffer);
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Pinned Object Heap - .NET 10 feature")]
public unsafe int UsePinnedObjectHeap()
{
// .NET 10: Pinned Object Heap - dedicated area for pinned objects
// Does not fragment regular GC heap
int sum = 0;
for (int i = 0; i < 1000; i++)
{
// GC.AllocateArray with pinned=true uses POH
var pinnedArray = GC.AllocateArray<byte>(4096, pinned: true);
fixed (byte* ptr = pinnedArray)
{
// Work with pinned pointer
for (int j = 0; j < 4096; j++)
{
ptr[j] = (byte)(ptr[j] + 1);
}
}
sum += pinnedArray.Length;
}
return sum;
}
[GlobalCleanup]
public void Cleanup()
{
_pinnedHandle.Free();
}
private class SmallObject
{
public int Id { get; set; }
public byte[] Data { get; set; } = null!;
}
}
1.3 Detecting and Preventing Pinned Object Issues
Pinning objects prevents the GC from moving them, causing heap fragmentation. In .NET 10, the Pinned Object Heap (POH) solves this.
// Vehixcare.Performance.Benchmarks/Memory/PinnedObjectBenchmarks.cs
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net100)]
public class PinnedObjectBenchmarks
{
private byte[] _largeArray;
private GCHandle _pinnedHandle;
private byte[] _pohArray;
[Params(1024, 10240, 102400, 1048576)] // 1KB to 1MB
public int ArraySize { get; set; }
[GlobalSetup]
public void Setup()
{
_largeArray = new byte[ArraySize];
_pohArray = GC.AllocateArray<byte>(ArraySize, pinned: true);
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Fixed statement - Temporary pinning")]
public unsafe void ProcessWithFixedStatement()
{
// fixed statement pins temporarily, released after scope
// No heap fragmentation because pinning is short-lived
fixed (byte* ptr = _largeArray)
{
ProcessBytes(ptr, _largeArray.Length);
}
}
[Benchmark]
[BenchmarkDescription("GCHandle.Alloc - Persistent pinning (causes fragmentation)")]
public unsafe void ProcessWithPersistentPinning()
{
// DANGER: Long-term pinning prevents GC compaction
// Causes heap fragmentation over time
_pinnedHandle = GCHandle.Alloc(_largeArray, GCHandleType.Pinned);
IntPtr ptr = _pinnedHandle.AddrOfPinnedObject();
ProcessBytes((byte*)ptr, _largeArray.Length);
// Don't forget to free - memory leak if not!
_pinnedHandle.Free();
}
[Benchmark]
[BenchmarkDescription("Pinned Object Heap - .NET 10 optimized")]
public unsafe void ProcessWithPinnedObjectHeap()
{
// .NET 10: Pinned Object Heap - dedicated area for pinned objects
// Does not fragment regular GC heap
// Critical for Vehixcare's native interop scenarios
fixed (byte* ptr = _pohArray)
{
ProcessBytes(ptr, _pohArray.Length);
}
}
[Benchmark]
[BenchmarkDescription("MemoryMarshal - No pinning required")]
public unsafe void ProcessWithMemoryMarshal()
{
// MemoryMarshal allows pointer access without pinning
// Best for scenarios where GC won't relocate (stackalloc, array pooling)
var span = _largeArray.AsSpan();
ref byte byteRef = ref MemoryMarshal.GetReference(span);
fixed (byte* ptr = &byteRef)
{
ProcessBytes(ptr, span.Length);
}
}
private unsafe void ProcessBytes(byte* ptr, int length)
{
// SIMD-optimized byte processing
if (Avx2.IsSupported)
{
var increment = Vector256.Create((byte)1);
for (int i = 0; i < length; i += 32)
{
var data = Avx.LoadVector256(ptr + i);
var result = Avx2.Add(data, increment);
Avx.Store(ptr + i, result);
}
}
else
{
for (int i = 0; i < length; i++)
{
ptr[i] = (byte)(ptr[i] + 1);
}
}
}
}
1.4 Native Memory Allocation for Critical Paths
For extreme performance scenarios where GC pressure is unacceptable, .NET 10 provides NativeMemory APIs.
// Vehixcare.Performance.Benchmarks/Memory/NativeMemoryBenchmarks.cs
using System.Runtime.InteropServices;
namespace Vehixcare.Performance.Benchmarks.Memory;
[SimpleJob(RuntimeMoniker.Net100)]
[MemoryDiagnoser]
public class NativeMemoryBenchmarks : IDisposable
{
private IntPtr _nativePtr;
private int[] _managedArray;
[Params(10000, 100000, 1000000)]
public int ElementCount { get; set; }
[GlobalSetup]
public void Setup()
{
_managedArray = new int[ElementCount];
_nativePtr = NativeMemory.Alloc((nuint)(ElementCount * sizeof(int)));
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Managed array - GC tracked, heap allocated")]
public int ProcessManagedArray()
{
int sum = 0;
for (int i = 0; i < _managedArray.Length; i++)
{
_managedArray[i] = i;
sum += _managedArray[i];
}
return sum;
}
[Benchmark]
[BenchmarkDescription("NativeMemory.Alloc - No GC overhead, manual management")]
public unsafe int ProcessNativeMemory()
{
int* ptr = (int*)_nativePtr;
int sum = 0;
for (int i = 0; i < ElementCount; i++)
{
ptr[i] = i;
sum += ptr[i];
}
return sum;
}
[Benchmark]
[BenchmarkDescription("NativeMemory.AlignedAlloc - Cache-line optimized")]
public unsafe int ProcessAlignedNativeMemory()
{
// 64-byte alignment for cache line optimization
// Critical for SIMD and multi-threaded scenarios
var alignedPtr = NativeMemory.AlignedAlloc((nuint)(ElementCount * sizeof(int)), 64);
try
{
int* ptr = (int*)alignedPtr;
int sum = 0;
for (int i = 0; i < ElementCount; i++)
{
ptr[i] = i;
sum += ptr[i];
}
return sum;
}
finally
{
NativeMemory.AlignedFree(alignedPtr);
}
}
[Benchmark]
[BenchmarkDescription("NativeMemory + SIMD - Ultimate performance")]
public unsafe int ProcessNativeMemorySIMD()
{
if (!Avx2.IsSupported)
return ProcessNativeMemory();
int* ptr = (int*)_nativePtr;
var sumVec = Vector256<int>.Zero;
for (int i = 0; i < ElementCount; i += 8)
{
// Load 8 integers at once
var data = Avx.LoadVector256(ptr + i);
sumVec = Avx2.Add(sumVec, data);
}
// Horizontal sum
var sum = sumVec.GetElement(0) + sumVec.GetElement(1) +
sumVec.GetElement(2) + sumVec.GetElement(3) +
sumVec.GetElement(4) + sumVec.GetElement(5) +
sumVec.GetElement(6) + sumVec.GetElement(7);
return sum;
}
[GlobalCleanup]
public void Cleanup()
{
NativeMemory.Free(_nativePtr);
}
public void Dispose()
{
Cleanup();
}
}
2.0 Hardware Counter Deep Dive
2.1 Understanding CPU Performance Counters
Hardware counters provide insight into what the CPU is actually doing, not just how long it takes.

2.2 Benchmarking with Hardware Counters
// Vehixcare.Performance.Benchmarks/Hardware/HardwareCounterBenchmarks.cs
using System.Numerics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace Vehixcare.Performance.Benchmarks.Hardware;
[SimpleJob(RuntimeMoniker.Net100)]
[HardwareCounters(
HardwareCounter.CacheMisses, // L1/L2/L3 cache misses
HardwareCounter.BranchMispredictions, // Incorrect branch predictions
HardwareCounter.InstructionRetired, // Total instructions executed
HardwareCounter.TotalIssues, // Instructions issued to pipeline
HardwareCounter.BranchInstructions, // Total branch instructions
HardwareCounter.TotalCycles // CPU cycles elapsed
)]
[DisassemblyDiagnoser(printSource: true, maxDepth: 5)]
public class HardwareCounterBenchmarks
{
private int[] _randomData;
private int[] _sortedData;
private int[] _sequentialData;
[Params(10000, 100000, 1000000)]
public int DataSize { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_randomData = Enumerable.Range(0, DataSize)
.Select(_ => random.Next())
.ToArray();
_sortedData = _randomData.OrderBy(x => x).ToArray();
_sequentialData = Enumerable.Range(0, DataSize).ToArray();
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Random data - High branch mispredictions")]
public int Sum_RandomData()
{
int sum = 0;
for (int i = 0; i < _randomData.Length; i++)
{
// Branch predictor struggles with random data
// Misprediction rate ~50%
if (_randomData[i] > 0)
{
sum += _randomData[i];
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Sorted data - Low branch mispredictions")]
public int Sum_SortedData()
{
int sum = 0;
for (int i = 0; i < _sortedData.Length; i++)
{
// Branch predictor learns pattern after first few iterations
// Misprediction rate ~1% after warmup
if (_sortedData[i] > 0)
{
sum += _sortedData[i];
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Sequential data - Perfect prediction")]
public int Sum_SequentialData()
{
int sum = 0;
for (int i = 0; i < _sequentialData.Length; i++)
{
// Always true - branch predictor predicts correctly every time
// Misprediction rate ~0%
if (_sequentialData[i] >= 0)
{
sum += _sequentialData[i];
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Branchless using CMOV - Zero mispredictions")]
public int Sum_Branchless()
{
int sum = 0;
for (int i = 0; i < _randomData.Length; i++)
{
// No branch - uses conditional move (CMOV) instruction
// 0 mispredictions, but may increase instruction count
sum += _randomData[i] > 0 ? _randomData[i] : 0;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("SIMD vectorized - Process 8 at once")]
public unsafe int Sum_SIMD()
{
if (!Avx2.IsSupported)
return Sum_RandomData();
var sumVec = Vector256<int>.Zero;
fixed (int* ptr = _randomData)
{
for (int i = 0; i < _randomData.Length; i += 8)
{
var dataVec = Avx.LoadVector256(ptr + i);
var zeroVec = Vector256<int>.Zero;
var mask = Avx2.CompareGreaterThan(dataVec, zeroVec);
var positive = Avx2.And(dataVec, mask);
sumVec = Avx2.Add(sumVec, positive);
}
}
// Horizontal sum of vector
return sumVec.GetElement(0) + sumVec.GetElement(1) +
sumVec.GetElement(2) + sumVec.GetElement(3) +
sumVec.GetElement(4) + sumVec.GetElement(5) +
sumVec.GetElement(6) + sumVec.GetElement(7);
}
[Benchmark]
[BenchmarkDescription("Lookup table - Eliminates computation")]
public int Sum_LookupTable()
{
// Precomputed table for small value ranges
int[] table = Enumerable.Range(0, 256).Select(i => i > 0 ? i : 0).ToArray();
int sum = 0;
foreach (var val in _randomData)
{
// Table lookup instead of branch or computation
sum += val < 256 ? table[val] : (val > 0 ? val : 0);
}
return sum;
}
}
2.3 Cache Locality and Memory Access Patterns
Understanding cache behavior is critical for high-performance systems. Poor cache locality can cause 100x slowdowns.
// Vehixcare.Performance.Benchmarks/Hardware/CacheLocalityBenchmarks.cs
[SimpleJob(RuntimeMoniker.Net100)]
[HardwareCounters(HardwareCounter.CacheMisses)]
public class CacheLocalityBenchmarks
{
private List<Node> _linkedNodes;
private Node[] _arrayNodes;
private int[] _indices;
private int[,] _matrix;
private int[] _matrixFlat;
[Params(10000, 100000, 1000000)]
public int NodeCount { get; set; }
[GlobalSetup]
public void Setup()
{
// Linked list - poor cache locality (nodes scattered)
_linkedNodes = new List<Node>();
for (int i = 0; i < NodeCount; i++)
{
_linkedNodes.Add(new Node { Value = i, Next = null });
}
for (int i = 0; i < NodeCount - 1; i++)
{
_linkedNodes[i].Next = _linkedNodes[i + 1];
}
// Array - excellent cache locality (contiguous memory)
_arrayNodes = new Node[NodeCount];
for (int i = 0; i < NodeCount; i++)
{
_arrayNodes[i] = new Node { Value = i };
}
// Random traversal order to test cache behavior
var random = new Random(42);
_indices = Enumerable.Range(0, NodeCount)
.OrderBy(_ => random.Next())
.ToArray();
// Matrix for row/column major tests
var size = (int)Math.Sqrt(NodeCount);
_matrix = new int[size, size];
_matrixFlat = new int[size * size];
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
_matrix[i, j] = i * j;
_matrixFlat[i * size + j] = i * j;
}
}
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Linked list traversal - Poor cache locality")]
public int Sum_LinkedList()
{
int sum = 0;
var current = _linkedNodes[0];
for (int i = 0; i < NodeCount; i++)
{
sum += current.Value;
current = current.Next!;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Array traversal - Excellent cache locality")]
public int Sum_Array()
{
int sum = 0;
for (int i = 0; i < NodeCount; i++)
{
sum += _arrayNodes[i].Value;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Random array access - Poor cache locality")]
public int Sum_RandomAccess()
{
int sum = 0;
foreach (var idx in _indices)
{
sum += _arrayNodes[idx].Value;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Span<T> - Optimized for sequential access")]
public int Sum_Span()
{
Span<Node> span = _arrayNodes.AsSpan();
int sum = 0;
foreach (ref var node in span)
{
sum += node.Value;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Matrix row-major - Good cache locality")]
public int Sum_MatrixRowMajor()
{
int size = (int)Math.Sqrt(NodeCount);
int sum = 0;
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
sum += _matrix[i, j];
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Matrix column-major - Poor cache locality")]
public int Sum_MatrixColumnMajor()
{
int size = (int)Math.Sqrt(NodeCount);
int sum = 0;
for (int j = 0; j < size; j++)
{
for (int i = 0; i < size; i++)
{
sum += _matrix[i, j]; // Strided access - cache misses
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Matrix flat array - Best cache locality")]
public int Sum_MatrixFlat()
{
int sum = 0;
foreach (var val in _matrixFlat)
{
sum += val;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Prefetch - Manual cache prefetching")]
public unsafe int Sum_WithPrefetch()
{
if (!Avx2.IsSupported)
return Sum_Array();
int sum = 0;
fixed (Node* ptr = _arrayNodes)
{
for (int i = 0; i < NodeCount; i++)
{
// Prefetch next 8 cache lines ahead
if (i + 64 < NodeCount)
{
_mm_prefetch((char*)(ptr + i + 64), _MM_HINT_T0);
}
sum += ptr[i].Value;
}
}
return sum;
}
[System.Runtime.Intrinsics.X86.Intrinsic]
private static extern void _mm_prefetch(char* p, int i);
private const int _MM_HINT_T0 = 1;
private class Node
{
public int Value;
public Node? Next;
}
}
2.4 Branch Prediction Analysis
Branch mispredictions are one of the biggest performance killers on modern CPUs.
// Vehixcare.Performance.Benchmarks/Hardware/BranchPredictionBenchmarks.cs
[SimpleJob(RuntimeMoniker.Net100)]
[HardwareCounters(HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)]
public class BranchPredictionBenchmarks
{
private int[] _data;
private int[] _thresholds;
[Params(1000000)]
public int DataSize { get; set; }
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_data = Enumerable.Range(0, DataSize).Select(_ => random.Next(0, 1000)).ToArray();
_thresholds = Enumerable.Range(0, 100).Select(i => i * 10).ToArray();
}
[Benchmark(Baseline = true)]
[BenchmarkDescription("Multiple branches - High misprediction")]
public int Process_MultipleBranches()
{
int sum = 0;
foreach (var val in _data)
{
// Multiple branches in sequence - hard to predict
if (val > 500)
{
sum += val;
}
if (val > 250 && val < 750)
{
sum -= val / 2;
}
if (val % 2 == 0)
{
sum += val / 4;
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Single branch - Easy to predict")]
public int Process_SingleBranch()
{
int sum = 0;
foreach (var val in _data)
{
// Single branch - predictor learns pattern quickly
if (val > 500)
{
sum += val;
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Branchless using arithmetic")]
public int Process_BranchlessArithmetic()
{
int sum = 0;
foreach (var val in _data)
{
// No branches - uses arithmetic (val > 500 ? val : 0)
// (val > 500) returns 1 or 0, multiply by val
sum += (val > 500 ? 1 : 0) * val;
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Switch statement - Jump table")]
public int Process_SwitchStatement()
{
int sum = 0;
foreach (var val in _data)
{
// Switch with many cases - uses jump table
switch (val % 10)
{
case 0: sum += val; break;
case 1: sum -= val; break;
case 2: sum += val * 2; break;
case 3: sum -= val / 2; break;
case 4: sum += val / 4; break;
case 5: sum -= val / 5; break;
case 6: sum += val / 6; break;
case 7: sum -= val / 7; break;
case 8: sum += val / 8; break;
default: sum += 0; break;
}
}
return sum;
}
[Benchmark]
[BenchmarkDescription("Dictionary lookup - Eliminates branching")]
public int Process_DictionaryLookup()
{
var operations = new Dictionary<int, Func<int, int>>
{
[0] = v => v,
[1] = v => -v,
[2] = v => v * 2,
[3] = v => -v / 2,
[4] = v => v / 4,
[5] = v => -v / 5,
[6] = v => v / 6,
[7] = v => -v / 7,
[8] = v => v / 8,
[9] = v => 0
};
int sum = 0;
foreach (var val in _data)
{
sum += operations[val % 10](val);
}
return sum;
}
}
3.0 Cross-Runtime Regression Testing
3.1 Multi-Runtime Benchmark Configuration
Testing across .NET 8, 9, and 10 ensures that upgrades don’t regress performance.
// Vehixcare.Performance.Benchmarks/CrossRuntime/CrossRuntimeBenchmarks.cs
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net100)]
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class CrossRuntimeBenchmarks
{
private TelemetryData[] _testData;
private int[] _largeArray;
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
_testData = Enumerable.Range(0, 1000)
.Select(i => new TelemetryData
{
VehicleId = $"VHC-{i:D4}",
Timestamp = DateTime.UtcNow,
Speed = random.Next(0, 180),
FuelLevel = random.NextDouble() * 100
}).ToArray();
_largeArray = Enumerable.Range(0, 1000000).Select(i => random.Next()).ToArray();
}
[Benchmark]
[BenchmarkDescription("JSON Serialization - Cross-runtime comparison")]
public byte[] Serialize_Json()
{
return JsonSerializer.SerializeToUtf8Bytes(_testData);
}
[Benchmark]
[BenchmarkDescription("MessagePack - Cross-runtime comparison")]
public byte[] Serialize_MessagePack()
{
return MessagePackSerializer.Serialize(_testData);
}
[Benchmark]
[BenchmarkDescription("LINQ query - Tests JIT improvements")]
public double Process_LINQ()
{
return _testData
.Where(t => t.Speed > 50)
.Average(t => t.FuelLevel);
}
[Benchmark]
[BenchmarkDescription("SIMD Sum - Tests vectorization improvements across runtimes")]
public int Sum_SIMD()
{
if (!Avx2.IsSupported)
return _largeArray.Sum();
var sumVec = Vector256<int>.Zero;
for (int i = 0; i < _largeArray.Length; i += 8)
{
var dataVec = Vector256.Create(
_largeArray[i], _largeArray[i + 1], _largeArray[i + 2], _largeArray[i + 3],
_largeArray[i + 4], _largeArray[i + 5], _largeArray[i + 6], _largeArray[i + 7]
);
sumVec = Avx2.Add(sumVec, dataVec);
}
return sumVec.GetElement(0) + sumVec.GetElement(1) + sumVec.GetElement(2) + sumVec.GetElement(3) +
sumVec.GetElement(4) + sumVec.GetElement(5) + sumVec.GetElement(6) + sumVec.GetElement(7);
}
[Benchmark]
[BenchmarkDescription("String manipulation - Tests PGO and inlining")]
public string Process_StringManipulation()
{
var sb = new StringBuilder();
foreach (var data in _testData.Take(100))
{
sb.Append(data.VehicleId).Append(':').Append(data.Speed).Append(';');
}
return sb.ToString();
}
[Benchmark]
[BenchmarkDescription("Exception handling - Tests EH improvements")]
public int Process_Exceptions()
{
int sum = 0;
for (int i = 0; i < 1000; i++)
{
try
{
sum += _testData[i].Speed;
}
catch
{
sum += 0;
}
}
return sum;
}
}
3.2 Performance Regression Test Suite
// Vehixcare.Performance.Tests/PerformanceRegressionTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Vehixcare.Performance.Tests;
[TestClass]
public class PerformanceRegressionTests
{
private BenchmarkRunner _runner;
private Dictionary<string, BenchmarkBaseline> _baselines;
[TestInitialize]
public void LoadBaselines()
{
_baselines = LoadBaselinesFromArtifacts("benchmark-baselines.json");
_runner = new BenchmarkRunner();
}
[TestMethod]
[TestCategory("Performance")]
public void TelemetryDeserialization_ShouldNotRegress()
{
var result = _runner.Run<TelemetryDeserializationBenchmarks>();
var baseline = _baselines["TelemetryDeserialization"];
var regression = (result.Mean - baseline.Mean) / baseline.Mean;
Assert.IsTrue(regression < 0.05,
$"Performance regressed by {regression:P1}. " +
$"Baseline: {baseline.Mean:F0} ns, Current: {result.Mean:F0} ns");
Console.WriteLine($"✓ Deserialization within 5%: {regression:P1} change");
}
[TestMethod]
[TestCategory("Performance")]
public void DriverScoring_Allocations_ShouldNotRegress()
{
var result = _runner.Run<DriverScoringBenchmarks>();
var baseline = _baselines["DriverScoring"];
var allocationRegression = (double)result.AllocatedBytes / baseline.AllocatedBytes;
Assert.IsTrue(allocationRegression <= 1.1,
$"Memory allocation increased from {baseline.AllocatedBytes} to {result.AllocatedBytes} bytes");
Console.WriteLine($"✓ Allocations within 10%: {allocationRegression:P0} of baseline");
}
[TestMethod]
[TestCategory("Performance")]
public void GeoFencing_SIMD_ShouldBeAccelerated()
{
var result = _runner.Run<SpatialBenchmarks>();
// Ensure SIMD is actually being used
var simdMethod = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("Vectorized"));
Assert.IsTrue(simdMethod.HardwareCounters.SIMDInstructions > 0,
"SIMD instructions not detected - vectorization may have failed");
// Verify speedup over scalar version
var scalarMethod = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("Haversine"));
var speedup = scalarMethod.ResultStatistics.Mean / simdMethod.ResultStatistics.Mean;
Assert.IsTrue(speedup > 2.0,
$"SIMD speedup insufficient: {speedup:F1}x (expected >2x)");
Console.WriteLine($"✓ SIMD speedup: {speedup:F1}x");
}
[TestMethod]
[TestCategory("Performance")]
public void MongoDBUpserts_BulkWrite_ShouldBeFaster()
{
var result = _runner.Run<MongoDBUpsertBenchmarks>();
var individual = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("Individual"));
var bulk = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("BulkUnordered"));
var speedup = individual.ResultStatistics.Mean / bulk.ResultStatistics.Mean;
Assert.IsTrue(speedup > 10.0,
$"Bulk write speedup insufficient: {speedup:F1}x (expected >10x)");
Console.WriteLine($"✓ Bulk write speedup: {speedup:F1}x");
}
[TestMethod]
[TestCategory("Performance")]
public void SignalRBroadcast_Grouping_ShouldBeFaster()
{
var result = _runner.Run<SignalRBroadcastBenchmarks>();
var allClients = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("AllClients"));
var grouped = result.Reports
.First(r => r.BenchmarkCase.Descriptor.WorkloadMethod.Name.Contains("Grouped"));
var speedup = allClients.ResultStatistics.Mean / grouped.ResultStatistics.Mean;
Assert.IsTrue(speedup > 5.0,
$"Group broadcast speedup insufficient: {speedup:F1}x (expected >5x)");
Console.WriteLine($"✓ Group broadcast speedup: {speedup:F1}x");
}
}
3.3 CI/CD Performance Gates
# .gitlab-ci.yml - Performance regression detection
performance-benchmark:
stage: performance
tags:
- dedicated-benchmark-runner
variables:
BASELINE_BRANCH: "main"
THRESHOLD_PERCENT: "5"
FAIL_ON_REGRESSION: "true"
script:
# Install .NET 10
- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version 10.0.100-preview.1
# Restore and build
- dotnet restore
- dotnet build -c Release --no-restore
# Run benchmarks
- dotnet run -c Release --project src/Vehixcare.Performance.Benchmarks/ \
--filter * --exporters json --artifact-path ./results
# Download baseline from main branch
- |
if [ "$CI_COMMIT_BRANCH" != "main" ]; then
curl --header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/$BASELINE_BRANCH/download?job=performance-benchmark" \
--output baseline.zip || echo "No baseline found"
if [ -f baseline.zip ]; then
unzip baseline.zip -d baseline/
fi
fi
# Compare with baseline
- |
dotnet run --project src/Vehixcare.Performance.Tools/ \
compare --baseline baseline/results/benchmark-results.json \
--current results/benchmark-results.json \
--threshold $THRESHOLD_PERCENT \
--fail-on-regression $FAIL_ON_REGRESSION \
--export-markdown performance-report.md \
--export-html performance-report.html
# Generate performance trend chart
- |
dotnet run --project src/Vehixcare.Performance.Tools/ \
trend --history benchmark-history.json \
--export-chart performance-trend.jpg
artifacts:
paths:
- results/
- performance-report.md
- performance-report.html
- performance-trend.jpg
reports:
performance: results/benchmark-results.json
expire_in: 30 days
only:
- main
- develop
- merge_requests
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH == "develop"'
3.4 Historical Trending with InfluxDB
// Vehixcare.Performance.Tools/BenchmarkTrendRecorder.cs
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Writes;
namespace Vehixcare.Performance.Tools;
public class BenchmarkTrendRecorder : IDisposable
{
private readonly InfluxDBClient _client;
private readonly string _bucket;
private readonly string _org;
public BenchmarkTrendRecorder(string url, string token, string bucket, string org)
{
_client = new InfluxDBClient(url, token);
_bucket = bucket;
_org = org;
}
public async Task RecordBenchmarkResult(BenchmarkResult result)
{
var point = PointData
.Measurement("benchmark_results")
.Tag("benchmark_name", result.BenchmarkName)
.Tag("runtime", result.RuntimeVersion)
.Tag("machine", Environment.MachineName)
.Tag("branch", GetCurrentBranch())
.Field("mean_ns", result.MeanNanoseconds)
.Field("std_dev_ns", result.StandardDeviation)
.Field("p95_ns", result.Percentile95)
.Field("p99_ns", result.Percentile99)
.Field("allocated_bytes", result.AllocatedBytes)
.Field("gen0_collections", result.Gen0Collections)
.Field("gen1_collections", result.Gen1Collections)
.Field("gen2_collections", result.Gen2Collections)
.Timestamp(DateTime.UtcNow, WritePrecision.Ns);
using var writeApi = _client.GetWriteApi();
writeApi.WritePoint(point, _bucket, _org);
}
public async Task<PerformanceTrend> GetTrend(string benchmarkName, int days = 30)
{
var fluxQuery = $@"
from(bucket: ""{_bucket}"")
|> range(start: -{days}d)
|> filter(fn: (r) => r._measurement == ""benchmark_results"")
|> filter(fn: (r) => r.benchmark_name == ""{benchmarkName}"")
|> filter(fn: (r) => r._field == ""mean_ns"")
|> aggregateWindow(every: 1d, fn: mean)
|> yield(name: ""mean"")
";
var tables = await _client.GetQueryApi().QueryAsync(fluxQuery, _org);
var trend = new PerformanceTrend
{
BenchmarkName = benchmarkName,
DataPoints = new List<TrendPoint>()
};
foreach (var table in tables)
{
foreach (var record in table.Records)
{
trend.DataPoints.Add(new TrendPoint
{
Timestamp = record.GetTime().GetValueOrDefault(),
Value = record.GetValue() as double? ?? 0
});
}
}
// Calculate trend
if (trend.DataPoints.Count >= 7)
{
var recent = trend.DataPoints.TakeLast(7).Average(p => p.Value);
var historical = trend.DataPoints.Take(trend.DataPoints.Count - 7).Average(p => p.Value);
trend.PercentChange = (recent - historical) / historical * 100;
trend.IsRegressing = trend.PercentChange > 5;
}
return trend;
}
public async Task GenerateReport(string outputPath)
{
var benchmarks = new[]
{
"TelemetryDeserialization",
"DriverScoring",
"GeoFencing",
"MongoDBUpsert",
"SignalRBroadcast"
};
var sb = new StringBuilder();
sb.AppendLine("# Vehixcare Performance Trend Report");
sb.AppendLine($"Generated: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
sb.AppendLine();
sb.AppendLine("## Summary");
sb.AppendLine();
foreach (var benchmark in benchmarks)
{
var trend = await GetTrend(benchmark, 30);
var emoji = trend.IsRegressing ? "🔴" : (trend.PercentChange < -5 ? "🟢" : "🟡");
sb.AppendLine($"- {emoji} **{benchmark}**: {trend.PercentChange:+.1}% over 30 days");
}
sb.AppendLine();
sb.AppendLine("## Detailed Trends");
sb.AppendLine();
foreach (var benchmark in benchmarks)
{
var trend = await GetTrend(benchmark, 30);
sb.AppendLine($"### {benchmark}");
sb.AppendLine($"- 30-day change: {trend.PercentChange:+.1}%");
sb.AppendLine($"- Current mean: {trend.DataPoints.LastOrDefault()?.Value:F0} ns");
sb.AppendLine($"- Baseline mean: {trend.DataPoints.FirstOrDefault()?.Value:F0} ns");
sb.AppendLine();
}
await File.WriteAllTextAsync(outputPath, sb.ToString());
}
private string GetCurrentBranch()
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "rev-parse --abbrev-ref HEAD",
RedirectStandardOutput = true,
UseShellExecute = false
};
using var process = Process.Start(psi);
return process?.StandardOutput.ReadToEnd().Trim() ?? "unknown";
}
public void Dispose()
{
_client?.Dispose();
}
}
public class PerformanceTrend
{
public string BenchmarkName { get; set; } = string.Empty;
public List<TrendPoint> DataPoints { get; set; } = new();
public double PercentChange { get; set; }
public bool IsRegressing { get; set; }
}
public class TrendPoint
{
public DateTime Timestamp { get; set; }
public double Value { get; set; }
}
4.0 Production Performance Monitoring
4.1 Integrating BenchmarkDotNet with OpenTelemetry
// Vehixcare.Observability/BenchmarkTelemetryIntegration.cs
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using System.Diagnostics.Metrics;
namespace Vehixcare.Observability;
public class BenchmarkTelemetryIntegration
{
private static readonly Meter Meter = new("Vehixcare.Benchmarks", "1.0.0");
private static readonly Histogram<double> BenchmarkDuration =
Meter.CreateHistogram<double>("benchmark.duration", "ns", "Benchmark execution time");
private static readonly Counter<long> BenchmarkAllocations =
Meter.CreateCounter<long>("benchmark.allocations", "bytes", "Memory allocated");
private static readonly Counter<long> BenchmarkRuns =
Meter.CreateCounter<long>("benchmark.runs", "count", "Number of benchmark executions");
private static readonly ObservableGauge<double> BenchmarkHealth =
Meter.CreateObservableGauge("benchmark.health", () => GetHealthScore());
public void RecordBenchmarkMetrics(BenchmarkResult result)
{
var tags = new TagList
{
{ "benchmark", result.BenchmarkName },
{ "runtime", result.RuntimeVersion },
{ "machine", Environment.MachineName },
{ "environment", GetEnvironment() }
};
BenchmarkDuration.Record(result.MeanNanoseconds, tags);
BenchmarkAllocations.Add(result.AllocatedBytes, tags);
BenchmarkRuns.Add(1, tags);
// Record percentiles separately
using var percentileTags = new TagList { { "benchmark", result.BenchmarkName } };
percentileTags.Add("percentile", "p95");
BenchmarkDuration.Record(result.Percentile95, percentileTags);
percentileTags.Remove("percentile");
percentileTags.Add("percentile", "p99");
BenchmarkDuration.Record(result.Percentile99, percentileTags);
}
private static double GetHealthScore()
{
// Calculate health based on recent benchmark results
// Returns 0-100 where 100 is perfect
var recentResults = GetRecentBenchmarks(TimeSpan.FromHours(1));
if (!recentResults.Any()) return 100;
var baseline = GetBaseline(recentResults.First().BenchmarkName);
var averageDegradation = recentResults
.Select(r => (r.MeanNanoseconds - baseline.MeanNanoseconds) / baseline.MeanNanoseconds)
.Average();
var healthScore = Math.Max(0, 100 - (averageDegradation * 100));
return Math.Min(100, healthScore);
}
private static string GetEnvironment()
{
return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
}
}
public class PerformanceHealthCheck : IHealthCheck
{
private readonly IMemoryCache _cache;
private readonly Dictionary<string, BenchmarkBaseline> _baselines;
public PerformanceHealthCheck(IMemoryCache cache)
{
_cache = cache;
_baselines = LoadBaselines();
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var issues = new List<string>();
var warnings = new List<string>();
foreach (var baseline in _baselines)
{
var current = await MeasureCurrentPerformance(baseline.Key);
var degradation = (current - baseline.Value.Mean) / baseline.Value.Mean;
if (degradation > 0.20) // 20% degradation - critical
{
issues.Add($"{baseline.Key}: {degradation:P1} slower than baseline (critical)");
}
else if (degradation > 0.10) // 10% degradation - warning
{
warnings.Add($"{baseline.Key}: {degradation:P1} slower than baseline (warning)");
}
else if (degradation < -0.05) // 5% improvement
{
Console.WriteLine($"✓ {baseline.Key}: {degradation:P1} faster than baseline");
}
}
if (issues.Any())
{
return HealthCheckResult.Unhealthy(string.Join("; ", issues));
}
if (warnings.Any())
{
return HealthCheckResult.Degraded(string.Join("; ", warnings));
}
return HealthCheckResult.Healthy("Performance within baseline");
}
private async Task<double> MeasureCurrentPerformance(string benchmarkName)
{
// Quick benchmark sample in production (1% of requests)
if (Random.Shared.NextDouble() > 0.01)
return GetCachedMetric(benchmarkName);
var sw = Stopwatch.StartNew();
await ExecuteBenchmarkSampleAsync(benchmarkName);
sw.Stop();
var latency = sw.Elapsed.TotalNanoseconds;
CacheMetric(benchmarkName, latency);
return latency;
}
}
4.2 Real-time Performance Dashboard with SignalR
// Vehixcare.API/Hubs/PerformanceDashboardHub.cs
using Microsoft.AspNetCore.SignalR;
using System.Threading.Channels;
namespace Vehixcare.API.Hubs;
public class PerformanceDashboardHub : Hub
{
private readonly IMemoryCache _cache;
private readonly PerformanceCollector _collector;
private readonly ILogger<PerformanceDashboardHub> _logger;
public PerformanceDashboardHub(
IMemoryCache cache,
PerformanceCollector collector,
ILogger<PerformanceDashboardHub> logger)
{
_cache = cache;
_collector = collector;
_logger = logger;
}
public async Task SubscribeToPerformanceMetrics()
{
_logger.LogInformation("Client {ConnectionId} subscribed to performance metrics", Context.ConnectionId);
// Create channel for streaming
var channel = Channel.CreateUnbounded<PerformanceSnapshot>();
// Start background producer
_ = Task.Run(async () =>
{
while (true)
{
try
{
var snapshot = new PerformanceSnapshot
{
Timestamp = DateTime.UtcNow,
Metrics = await _collector.GetCurrentMetrics(),
BaselineComparison = GetBaselineComparison(),
Anomalies = _collector.GetRecentAnomalies(TimeSpan.FromMinutes(5)),
Predictions = await PredictNextHour()
};
await channel.Writer.WriteAsync(snapshot);
await Task.Delay(1000); // 1 Hz updates
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating performance snapshot");
}
}
});
// Stream to client
await Clients.Caller.SendAsync("PerformanceStream", channel.Reader.ReadAllAsync());
}
public async Task<HistoricalReport> GetHistoricalReport(DateTime start, DateTime end)
{
return await _collector.GenerateReport(start, end);
}
public async Task<Dictionary<string, double>> GetCurrentMetrics()
{
return await _collector.GetCurrentMetrics();
}
private Dictionary<string, double> GetBaselineComparison()
{
var baseline = LoadBaselines();
var current = _collector.GetLatestMetrics();
return baseline.ToDictionary(
b => b.Key,
b => (current[b.Key] - b.Value) / b.Value * 100 // Percent difference
);
}
private async Task<PerformancePrediction> PredictNextHour()
{
// Use ML model to predict next hour's performance
var model = await LoadPredictionModel();
var recent = _collector.GetRecentMetrics(TimeSpan.FromMinutes(30));
return model.PredictNextHour(recent);
}
}
public class PerformanceSnapshot
{
public DateTime Timestamp { get; set; }
public Dictionary<string, double> Metrics { get; set; } = new();
public Dictionary<string, double> BaselineComparison { get; set; } = new();
public List<PerformanceAnomaly> Anomalies { get; set; } = new();
public PerformancePrediction Predictions { get; set; } = new();
}
public class PerformanceAnomaly
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string MetricName { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public double Value { get; set; }
public double ExpectedValue { get; set; }
public double Deviation { get; set; }
public string Severity { get; set; } = "Warning";
}
5.0 Real-world Optimization Case Studies
5.1 Case Study: Telemetry Deserialization (10.4x Improvement)
Problem: Vehixcare was spending 1,021 ns per message on JSON deserialization, causing CPU saturation at 10,000 msg/sec (10.2ms CPU per second, leaving only 50% headroom).
Analysis with BenchmarkDotNet:
[MemoryDiagnoser]
[HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions)]
public class TelemetryDeserializationDiagnosis
{
private byte[] _jsonData;
[Benchmark(Baseline = true)]
public TelemetryData Deserialize_Original()
{
return JsonSerializer.Deserialize<TelemetryData>(_jsonData);
}
// Results showed:
// - Mean: 1,021 ns
// - 640 bytes allocated per operation (12.8 MB/sec at 20k msg/sec)
// - 15% cache miss rate (L1 misses)
// - 45% time spent in reflection (System.Text.Json.JsonSerializer.HandleNull)
// - 30% time spent in string allocation
// - 12% branch misprediction rate (switch on property names)
}
Root Causes Identified:
- Reflection-based deserialization (45% of time)
- String allocations for every property (30% of time)
- Switch statement on property names causing branch mispredictions (12% of time)
- Poor cache locality (15% cache misses)
Solution Applied:
// Step 1: Source generators to eliminate reflection
[JsonSerializable(typeof(TelemetryData))]
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
private partial class TelemetryJsonContext : JsonSerializerContext { }
// Step 2: Use MemoryPack for zero-copy deserialization
[MemoryPackable]
public partial class TelemetryData { }
// Step 3: Implement custom parser with Utf8JsonReader
public static TelemetryData Parse(ref Utf8JsonReader reader)
{
var result = new TelemetryData { VehicleId = string.Empty };
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType == JsonTokenType.PropertyName)
{
var property = reader.GetString();
reader.Read();
// Use direct comparison instead of switch
if (property == "vehicleId")
result.VehicleId = reader.GetString()!;
else if (property == "speed")
result.Speed = reader.GetDecimal();
// ... direct comparisons are branch-predictor friendly
}
}
return result;
}
Final Results:

5.2 Case Study: Geo-fencing Hot Path (13.2x Improvement)
Problem: Checking 1,000 vehicles against 500 geofences took 450ms (450μs per vehicle) → too slow for real-time alerts (target 50μs per vehicle).
Analysis revealed: 89% of time spent in Math.Sin and Math.Cos calls (Haversine formula), 11% in loop overhead.
Solution Applied:
// Before: Haversine with trigonometric functions - 450ns per check
double HaversineDistance(GeoCoordinate a, GeoCoordinate b)
{
const double R = 6371000;
var dLat = ToRadians(b.Latitude - a.Latitude);
var dLon = ToRadians(b.Longitude - a.Longitude);
var lat1 = ToRadians(a.Latitude);
var lat2 = ToRadians(b.Latitude);
var aVal = Math.Sin(dLat/2) * Math.Sin(dLat/2) +
Math.Sin(dLon/2) * Math.Sin(dLon/2) * Math.Cos(lat1) * Math.Cos(lat2);
var c = 2 * Math.Atan2(Math.Sqrt(aVal), Math.Sqrt(1-aVal));
return R * c;
}
// After: Equirectangular approximation + spatial grid
public class OptimizedGeoFenceChecker
{
private readonly Dictionary<(int, int), List<GeoFence>> _spatialGrid;
private const double EARTH_RADIUS_M = 6371000;
private const double METERS_PER_DEGREE_LAT = 111319.9;
public bool IsInAnyFence(GeoCoordinate vehicle)
{
// Step 1: Determine grid cell (5km cells)
var cellX = (int)(vehicle.Latitude / 0.045); // ~5km in degrees
var cellY = (int)(vehicle.Longitude / 0.045);
// Step 2: Only check nearby cells (3x3 grid)
for (int dx = -1; dx <= 1; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
var key = (cellX + dx, cellY + dy);
if (_spatialGrid.TryGetValue(key, out var fences))
{
foreach (var fence in fences)
{
if (FastDistanceCheck(vehicle, fence) <= fence.Radius)
return true;
}
}
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double FastDistanceCheck(GeoCoordinate a, GeoFence fence)
{
// Equirectangular approximation (0.5% error for distances <100km)
var dx = (a.Longitude - fence.Center.Longitude) *
Math.Cos((a.Latitude + fence.Center.Latitude) * Math.PI / 360);
var dy = a.Latitude - fence.Center.Latitude;
return Math.Sqrt(dx * dx + dy * dy) * METERS_PER_DEGREE_LAT;
}
}
Final Results:

5.3 Case Study: MongoDB Write Bottleneck (14.3x Improvement)
Problem: Writing 1,000 telemetry records took 5 seconds (5ms per document) → falling behind real-time ingestion (1000 msg/sec requires 1ms per write).
Benchmark revealed:
[Benchmark(Baseline = true)]
public async Task IndividualUpserts() // 5,000ms for 1,000 docs
{
foreach (var doc in batch)
{
await collection.ReplaceOneAsync(
Builders<VehicleTelemetry>.Filter
.Where(t => t.VehicleId == doc.VehicleId && t.Timestamp == doc.Timestamp),
doc,
new ReplaceOptions { IsUpsert = true }
);
}
}
Root cause: 1,000 network round trips instead of one. Each round trip adds ~5ms latency (network + MongoDB processing).
Solution Applied:
[Benchmark]
public async Task BulkUnordered() // 350ms for 1,000 docs (14.3x faster)
{
var models = new List<WriteModel<VehicleTelemetry>>(batch.Length);
foreach (var telemetry in batch)
{
var filter = Builders<VehicleTelemetry>.Filter
.Where(t => t.VehicleId == telemetry.VehicleId &&
t.Timestamp == telemetry.Timestamp);
var update = Builders<VehicleTelemetry>.Update
.Set(t => t.Speed, telemetry.Speed)
.Set(t => t.Latitude, telemetry.Latitude)
.Set(t => t.Longitude, telemetry.Longitude)
.Set(t => t.FuelLevel, telemetry.FuelLevel)
.Set(t => t.EngineTemp, telemetry.EngineTemp)
.Set(t => t.BatteryVoltage, telemetry.BatteryVoltage);
models.Add(new UpdateOneModel<VehicleTelemetry>(filter, update)
{
IsUpsert = true
});
}
await collection.BulkWriteAsync(models, new BulkWriteOptions
{
IsOrdered = false // Parallel execution
});
}
Final Results:

6.0 Custom Benchmarking Attributes for Vehixcare
6.1 Domain-Specific Benchmark Attribute
// Vehixcare.Performance.Attributes/TelemetryBenchmarkAttribute.cs
namespace Vehixcare.Performance.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class TelemetryBenchmarkAttribute : Attribute
{
public string Category { get; }
public int VehicleCount { get; set; } = 1000;
public int MessageRatePerSecond { get; set; } = 100;
public bool SimulateNetworkLatency { get; set; } = false;
public int NetworkLatencyMs { get; set; } = 0;
public bool SimulateJitter { get; set; } = false;
public double DuplicateRate { get; set; } = 0.0;
public int BatchSize { get; set; } = 100;
public bool RecordMemory { get; set; } = true;
public bool RecordHardwareCounters { get; set; } = true;
public TelemetryBenchmarkAttribute(string category)
{
Category = category;
}
}
// Usage
[TelemetryBenchmark("Ingestion", VehicleCount = 10000, MessageRatePerSecond = 1000)]
[Benchmark]
public async Task ProcessTelemetryBatch()
{
var processor = new TelemetryProcessor();
await processor.ProcessBatch(_testData);
}
6.2 Performance Budget Validator
// Vehixcare.Performance.Attributes/PerformanceBudgetAttribute.cs
[AttributeUsage(AttributeTargets.Method)]
public class PerformanceBudgetAttribute : Attribute
{
public double MaxMeanNanoseconds { get; set; }
public long MaxAllocatedBytes { get; set; }
public int MaxGen0Collections { get; set; }
public int MaxGen1Collections { get; set; }
public double MaxP95Nanoseconds { get; set; }
public double MaxP99Nanoseconds { get; set; }
public double MaxCacheMissRate { get; set; }
public double MaxBranchMispredictionRate { get; set; }
public PerformanceBudgetAttribute(double maxMeanNanoseconds)
{
MaxMeanNanoseconds = maxMeanNanoseconds;
}
}
// Post-benchmark validation
public class BudgetValidator
{
public BudgetValidationResult Validate(Summary summary)
{
var results = new BudgetValidationResult();
foreach (var report in summary.Reports)
{
var attribute = report.BenchmarkMethod.GetCustomAttribute<PerformanceBudgetAttribute>();
if (attribute == null) continue;
var mean = report.ResultStatistics.Mean;
if (mean > attribute.MaxMeanNanoseconds)
{
results.Failures.Add(new BudgetViolation
{
Benchmark = report.BenchmarkCase.DisplayName,
Metric = "Mean",
Budget = attribute.MaxMeanNanoseconds,
Actual = mean,
Excess = (mean - attribute.MaxMeanNanoseconds) / attribute.MaxMeanNanoseconds
});
}
var allocated = report.GcStats.AllocatedBytesPerOperation;
if (allocated > attribute.MaxAllocatedBytes)
{
results.Failures.Add(new BudgetViolation
{
Benchmark = report.BenchmarkCase.DisplayName,
Metric = "Allocated Bytes",
Budget = attribute.MaxAllocatedBytes,
Actual = allocated,
Excess = (allocated - attribute.MaxAllocatedBytes) / (double)attribute.MaxAllocatedBytes
});
}
}
return results;
}
}
7.0 Continuous Performance Testing Pipeline

7.1 Performance Regression Test Suite
// Vehixcare.Performance.Tests/ContinuousPerformanceTests.cs
[TestClass]
public class ContinuousPerformanceTests
{
private static readonly Dictionary<string, PerformanceBudget> Budgets = new()
{
["TelemetryDeserialization"] = new() { MeanNs = 100, AllocatedBytes = 64 },
["DriverScoring"] = new() { MeanNs = 2000, AllocatedBytes = 256 },
["GeoFencing"] = new() { MeanNs = 50, AllocatedBytes = 0 },
["MongoDBUpsert"] = new() { MeanMs = 0.5, AllocatedBytes = 1024 },
["SignalRBroadcast"] = new() { MeanUs = 50, AllocatedBytes = 32 }
};
[TestMethod]
[TestCategory("ContinuousPerformance")]
public void AllBenchmarks_ShouldMeetBudget()
{
var runner = new BenchmarkRunner();
var results = runner.RunAll();
foreach (var result in results)
{
var budget = Budgets[result.BenchmarkName];
Assert.IsTrue(result.MeanNanoseconds <= budget.MeanNs,
$"{result.BenchmarkName}: {result.MeanNanoseconds:F0}ns > {budget.MeanNs}ns budget");
Assert.IsTrue(result.AllocatedBytes <= budget.AllocatedBytes,
$"{result.BenchmarkName}: {result.AllocatedBytes}B > {budget.AllocatedBytes}B budget");
}
}
[TestMethod]
[TestCategory("ContinuousPerformance")]
public void Performance_ShouldNotRegressFromPreviousRun()
{
var current = RunBenchmarks();
var previous = LoadPreviousRun();
foreach (var benchmark in current.Keys)
{
var regression = (current[benchmark] - previous[benchmark]) / previous[benchmark];
Assert.IsTrue(regression < 0.05,
$"{benchmark} regressed by {regression:P1} from previous run");
}
}
}
8.0 Conclusion & Key Takeaways
8.1 Advanced Techniques Mastered

8.2 Key Takeaways from This Story

8.3 Final Best Practices
Do:
- ✅ Monitor GC generations — track Gen0/1/2 collection counts
- ✅ Use hardware counters — understand why code is slow
- ✅ Test across runtimes — .NET 8/9/10 before upgrading
- ✅ Set performance budgets — fail CI on regression
- ✅ Monitor in production — real-time anomaly detection
- ✅ Use POH for long-term pinning — .NET 10 feature
- ✅ Profile with eBPF — near-zero overhead production profiling
Don’t:
- ❌ Ignore memory allocations — Gen2 collections are expensive
- ❌ Guess about cache behavior — measure with hardware counters
- ❌ Assume upgrades improve performance — test across runtimes
- ❌ Skip regression testing — performance debt accumulates
- ❌ Use persistent pinning on regular heap — fragments memory
- ❌ Allocate native memory without freeing — memory leaks
📖 Complete Series Navigation
• **BenchmarkDotNet With .NET 10 Perf Optimization — Foundations & Methodology for C# Devs — Part 1** — BenchmarkDotNet fundamentals, SOLID patterns, 5 benchmark areas, quick wins (MessagePack 3.6x, bulk MongoDB 14.3x, SignalR grouping 55x) ✅ Published
• **BenchmarkDotNet With .NET 10 Perf Optimization — Advanced Performance Engineering Guide — Part 2** — Memory diagnostics, hardware counters, cross-runtime testing, production monitoring, real-world case studies (you are here) ✅ Published
• BenchmarkDotNet With .NET 10 Perf Optimization — AI-Powered Performance Engineering — Part 3 — ML performance prediction, distributed Kubernetes benchmarks, energy profiling, chaos engineering, custom hardware counters
• BenchmarkDotNet With .NET 10 Perf Optimization — The Future of Performance Tuning — Part 4 — Quantum computing, WebAssembly, eBPF kernel profiling, auto-fix generation, cross-cloud benchmarking, carbon-aware scheduling
Document Version: 2.0 Part: 2 of 4 Last Updated: April 2026 Target Framework: .NET 10.0 BenchmarkDotNet Version: 0.14.0
📎 Read BenchmarkDotNet With .NET 10 Perf Optimization — Foundations & Methodology for C# Devs — Part 1
📎 Read BenchmarkDotNet With .NET 10 Perf Optimization — AI-Powered Performance Engineering — Part 3
📎 Read BenchmarkDotNet With .NET 10 Perf Optimization — The Future of Performance Tuning — Part 4
Coming soon! Want it sooner? Let me know with a clap or comment below*
*� Questions? Drop a response — I read and reply to every comment.*📌 Save this story to your reading list — it helps other engineers discover it.🔗 Follow me →
Medium — mvineetsharma.medium.com
LinkedIn — linkedin.com/in/vineet-sharma-architect
In-depth .NET, Node.js, Python, Cloud Architecture, and System Design. New articles weekly
메타데이터
- post_id
- 8f99542e2ee2
- slug
- benchmarkdotnet-with-net-10-perf-optimization-advanced-performance-engineering-guide-part-2-8f99542e2ee2
- url
- https://medium.com/@mvineetsharma/benchmarkdotnet-with-net-10-perf-optimization-advanced-performance-engineering-guide-part-2-8f99542e2ee2
- canonical_url
- https://medium.com/@mvineetsharma/benchmarkdotnet-with-net-10-perf-optimization-advanced-performance-engineering-guide-part-2-8f99542e2ee2
- author_url
- https://medium.com/@mvineetsharma
- status
- ok
- fetched_at
- 2026-06-21 15:33:18