Index · Additional notes

2 min read
Senior6 min read
Rapid overview

Additional notes

1️⃣ What it means

Allocation discipline means designing your code so that you:

  • Allocate only when necessary
  • Reuse what you already allocated
  • Minimize copying of data
  • Keep object lifetimes short (so they die in Gen 0)
  • Prevent accidental heap allocations in tight loops or latency-sensitive paths

Basically:

“Don’t let your code throw objects at the GC faster than it can clean them up.”


4️⃣ Reuse patterns that eliminate GC churn

Object pooling

.NET has built-in pools for common cases:

using Microsoft.Extensions.ObjectPool;

var pool = ObjectPool.Create<MyReusableObject>();
var item = pool.Get();
// use item...
pool.Return(item);

💡 Great for: serializers, parsers, StringBuilders, temp containers.

Example:

var sb = StringBuilderCache.Acquire();
// build a string
var result = StringBuilderCache.GetStringAndRelease(sb);

→ zero allocations between calls.


Buffer pooling

The ArrayPool<T> API lets you rent and return arrays instead of allocating new ones.

var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
// use it
pool.Return(buffer);

💡 Use this for:

  • I/O buffers
  • Network streams
  • Deserialization
  • Message batching

🚀 Benefit: Avoids Large Object Heap churn (LOH fragmentation) and constant GC pressure.


String interning or caching

Instead of creating new string instances for common identifiers (e.g., “EURUSD”):

string symbol = string.Intern("EURUSD");

Or better — store common symbols in a static Dictionary<string, string> and reuse the reference.


Structs and value types

For small, immutable data (ticks, coordinates, etc.), use structs:

  • Stored inline → no GC tracking
  • Can live and die on the stack
  • No heap allocations for short-lived data
readonly struct Tick
{
    public string Symbol { get; }
    public double Bid { get; }
    public double Ask { get; }
}

But ⚠️ keep them small (≤ 16–32 bytes). Large structs hurt performance due to copy costs.


Using Span<T> / Memory<T> for zero-copy

Span<T> and Memory<T> let you operate directly on existing memory — without allocating new arrays or substrings.

Example: parsing a price line

ReadOnlySpan<byte> span = Encoding.ASCII.GetBytes("EURUSD,1.0743,1.0745");

int comma = span.IndexOf((byte)',');
var symbol = Encoding.ASCII.GetString(span[..comma]); // one allocation

Utf8Parser.TryParse(span[(comma + 1)..], out double bid, out _);

No string splitting, no array allocations, no GC.

💡 Rule: Use Span<T> for synchronous parsing; Memory<T> when data crosses async boundaries.


5️⃣ Avoiding hidden allocations

Even code that looks innocent can allocate. Some hidden examples:

CodeHidden allocation
foreach (var x in list)Enumerator struct may box
async methodsAllocates a state machine object
lambda or delegate captures variableAllocates closure object
ToString()Often allocates new string
Task.FromResult(...)Reuses task, good ✅
await on Task that already completedAllocates continuation unless optimized

💡 Use tools like:

dotnet-trace collect --process-id <pid>
dotnet-counters monitor System.Runtime

to watch Allocated Bytes/sec.


6️⃣ Temporal allocation awareness (lifetime patterns)

The key to designing allocation-efficient systems is understanding lifetime scopes:

LifetimeStrategy
Per-requestAvoid allocations in controllers; reuse service-scoped resources
Per-sessionUse dependency injection scopes for per-user data
Global/staticCache immutable data, don’t recreate
TransientKeep short-lived structs or pooled objects

Example: in a market data service

  • Buffer per connection (rented from pool)
  • Parser per connection (reused object)
  • Tick structs per message (stack-allocated)

No GC churn in steady state.


7️⃣ Measuring & validating allocation discipline

Use:

dotnet-counters monitor System.Runtime

Watch:

Allocated Bytes/sec
Gen 0 GC Count
% Time in GC

or use code:

Console.WriteLine(GC.GetTotalAllocatedBytes(true));

✅ Healthy pattern:

  • High throughput with low Allocated Bytes/sec
  • Frequent Gen0, rare Gen1/2
  • % Time in GC < 2–3%

See also