Index · Additional notes
2 min read- Additional notes
- 1️⃣ What it means
- 4️⃣ Reuse patterns that eliminate GC churn
- ✅ **Object pooling**
- ✅ **Buffer pooling**
- ✅ **String interning or caching**
- ✅ **Structs and value types**
- ✅ **Using `Span
` / `Memory ` for zero-copy** - 5️⃣ Avoiding hidden allocations
- 6️⃣ Temporal allocation awareness (lifetime patterns)
- 7️⃣ Measuring & validating allocation discipline
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:
| Code | Hidden allocation |
|---|---|
foreach (var x in list) | Enumerator struct may box |
async methods | Allocates a state machine object |
lambda or delegate captures variable | Allocates closure object |
ToString() | Often allocates new string |
Task.FromResult(...) | Reuses task, good ✅ |
await on Task that already completed | Allocates 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:
| Lifetime | Strategy |
|---|---|
| Per-request | Avoid allocations in controllers; reuse service-scoped resources |
| Per-session | Use dependency injection scopes for per-user data |
| Global/static | Cache immutable data, don’t recreate |
| Transient | Keep 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%