2026-03-24 10:02:54 +00:00
|
|
|
---
|
|
|
|
|
name: csharp-optimization
|
2026-03-24 10:17:52 +00:00
|
|
|
description: >-
|
|
|
|
|
Use when optimizing C# code in MCC, reducing GC pressure, profiling hot paths,
|
|
|
|
|
fixing latency spikes, or reviewing code for allocation or throughput issues.
|
|
|
|
|
metadata:
|
|
|
|
|
category: technique
|
|
|
|
|
triggers: performance, allocations, GC, hot path, latency, throughput,
|
|
|
|
|
memory pressure, optimize, slow, freeze, lag spike, packet processing speed
|
2026-03-24 10:02:54 +00:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# C# Performance Optimization for MCC
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Hands-on optimization recipes for Minecraft Console Client hot paths.
|
|
|
|
|
Complements `csharp-best-practices` (conventions) with measurement-driven
|
|
|
|
|
performance work.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## When to Use
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
- Profiling or reducing GC pressure in a running MCC session
|
|
|
|
|
- Optimizing per-packet code (`Protocol18.HandlePacket`, `DataTypes.ReadNext*`)
|
|
|
|
|
- Optimizing per-tick code (`PlayerPhysics.Tick`, `CollisionDetector.Collide`)
|
|
|
|
|
- Speeding up chunk decoding (`Protocol18Terrain.ProcessChunkColumnData`)
|
|
|
|
|
- Improving A* pathfinding (`Movement.CalculatePath`)
|
|
|
|
|
- Reviewing any code change for allocation or throughput regressions
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
**NOT for:**
|
|
|
|
|
- Login, config parsing, or one-shot command handlers (prefer clarity there)
|
|
|
|
|
- Style/convention questions (use `csharp-best-practices` instead)
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Iron Rule: Measure First
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
**NEVER optimize without profiling data.**
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Guessing which code is slow is wrong more often than right. Measure, change,
|
|
|
|
|
re-measure. If you cannot show a before/after number, the optimization is not
|
|
|
|
|
justified.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
| Rationalization | Reality |
|
|
|
|
|
|-----------------|---------|
|
|
|
|
|
| "This is obviously slow" | Obvious to you is not obvious to the JIT. Measure. |
|
|
|
|
|
| "I'll profile later" | Later never comes. Profile now or don't optimize. |
|
|
|
|
|
| "It's just one allocation" | On a 20 TPS tick, one allocation = 20 per second = GC pressure. Measure. |
|
|
|
|
|
| "AggressiveInlining everywhere" | The JIT already inlines small methods. Prove it helps before adding. |
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## MCC Hot-Path Map
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Know which code runs at which frequency before deciding where to invest:
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
| Frequency | Key paths (actual files) | Priority |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Per-packet (100s/sec) | `Protocol/Handlers/Protocol18.cs` HandlePacket, `Protocol/Handlers/DataTypes.cs` ReadNext* | **High** |
|
|
|
|
|
| Per-tick (20/sec) | `Physics/PlayerPhysics.cs` Tick, `Physics/CollisionDetector.cs` Collide, ChatBot `Update()` | **High** |
|
|
|
|
|
| Per-chunk-load | `Protocol/Handlers/Protocol18Terrain.cs` ProcessChunkColumnData, ReadBlockStatesField | Medium |
|
|
|
|
|
| Per-pathfind | `Mapping/Movement.cs` CalculatePath (A*) | Medium |
|
|
|
|
|
| Per-connection | Login, registry sync, config | Low |
|
|
|
|
|
| Per-user-action | Commands, chat | Low |
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Profiling Recipes
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### 1. Live GC monitoring
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
```bash
|
|
|
|
|
dotnet-counters ps # find MinecraftClient PID
|
|
|
|
|
dotnet-counters monitor --process-id <PID> \
|
|
|
|
|
--counters System.Runtime[gen-0-gc-count,gen-1-gc-count,gen-2-gc-count,alloc-rate]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Healthy idle MCC: near-zero Gen-1/Gen-2 collections. Frequent Gen-0 during idle
|
|
|
|
|
means a hot-path allocation needs attention.
|
|
|
|
|
|
|
|
|
|
### 2. Allocation tracking
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
```bash
|
2026-03-24 10:17:52 +00:00
|
|
|
dotnet-trace collect --process-id <PID> \
|
2026-03-24 10:02:54 +00:00
|
|
|
--providers Microsoft-Windows-DotNETRuntime:0x1:5
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Open `.nettrace` in PerfView to find top-allocated types and call stacks.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### 3. Isolated benchmarks (BenchmarkDotNet)
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Extract the hot method, add `[MemoryDiagnoser]`. Key columns: **Mean**,
|
|
|
|
|
**Allocated**, **Gen0**.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Allocation Reduction (Highest Impact)
|
|
|
|
|
|
|
|
|
|
Reducing GC pressure directly reduces latency spikes in a long-running client.
|
|
|
|
|
|
|
|
|
|
### Pattern: Reuse per-tick buffers
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
// BEFORE: new List every tick (20 allocations/sec)
|
|
|
|
|
var result = new List<Aabb>();
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
// AFTER: thread-local reuse (0 allocations/sec)
|
|
|
|
|
[ThreadStatic] private static List<Aabb>? t_buf;
|
|
|
|
|
var result = t_buf ??= new List<Aabb>(64);
|
|
|
|
|
result.Clear();
|
2026-03-24 10:02:54 +00:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
`[ThreadStatic]` works when single-threaded and non-reentrant (physics tick).
|
|
|
|
|
If reentrant: use `ObjectPool<T>`. If cross-thread: use `ArrayPool<T>`.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Pattern: stackalloc for small fixed buffers
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
MCC already does this in `DataTypes.cs` for endian-swapped reads:
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
Span<byte> rawValue = stackalloc byte[8];
|
|
|
|
|
for (int i = 7; i >= 0; --i) rawValue[i] = cache.Dequeue();
|
|
|
|
|
return BitConverter.ToDouble(rawValue);
|
2026-03-24 10:02:54 +00:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Rules: under 512 bytes, known size at compile time, never inside loops or recursion.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Pattern: Span slicing instead of array copies
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
// BEFORE: allocates
|
2026-03-24 10:02:54 +00:00
|
|
|
byte[] sub = new byte[length];
|
|
|
|
|
Array.Copy(source, offset, sub, 0, length);
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
// AFTER: zero-copy
|
2026-03-24 10:02:54 +00:00
|
|
|
ReadOnlySpan<byte> sub = source.AsSpan(offset, length);
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Critical in packet parsing where many fields are sliced from one buffer.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Hot-Path Tuning
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
### MethodImpl attributes
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
MCC uses `[MethodImpl]` on its hottest paths. Match the attribute to the method:
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
| Attribute | When | MCC examples |
|
2026-03-24 10:02:54 +00:00
|
|
|
|---|---|---|
|
2026-03-24 10:17:52 +00:00
|
|
|
| `AggressiveInlining` | Tiny methods (< ~32 bytes IL), called millions of times | `Vec3d.Add`, `Aabb.Intersects`, `Chunk.SetWithoutCheck` |
|
|
|
|
|
| `AggressiveOptimization` | Larger critical-path methods | `ReadBlockStatesField`, `ProcessChunkColumnData` |
|
|
|
|
|
| Both | Medium methods, very high frequency | `DataTypes.ReadNextVarInt`, `ReadDataReverse` |
|
|
|
|
|
| Neither | Infrequent code | Login, config, commands |
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
**Do not scatter `AggressiveInlining` without profiling evidence.** The JIT
|
|
|
|
|
already inlines small methods.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
### BinaryPrimitives over BitConverter
|
|
|
|
|
|
|
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
// BEFORE: manual endian swap
|
2026-03-24 10:04:28 +00:00
|
|
|
(buf[0], buf[3]) = (buf[3], buf[0]);
|
2026-03-24 10:02:54 +00:00
|
|
|
int val = BitConverter.ToInt32(buf);
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
// AFTER: direct big-endian read, no branch
|
2026-03-24 10:02:54 +00:00
|
|
|
int val = BinaryPrimitives.ReadInt32BigEndian(buf);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### MemoryMarshal for bulk reads
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Already used in chunk decoding for zero-copy packed-long reads:
|
2026-03-24 10:02:54 +00:00
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
ReadOnlySpan<long> longs = MemoryMarshal.Cast<byte, long>(entryData);
|
2026-03-24 10:02:54 +00:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Data Structure Selection
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Frozen collections for palettes
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Palette maps are built once and read millions of times. `FrozenDictionary`
|
|
|
|
|
gives ~50% faster reads than `Dictionary`:
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
private static readonly FrozenDictionary<int, Material> s_palette =
|
|
|
|
|
new Dictionary<int, Material> { ... }.ToFrozenDictionary();
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Apply to: `BlockPalettes/*.cs`, `EntityPalettes/*.cs`, `ItemPalettes/*.cs`,
|
|
|
|
|
`PacketPalettes/*.cs`, any `static readonly Dictionary` populated once.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### PriorityQueue for A*
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
`Movement.cs` has a custom `BinaryHeap`. The built-in `PriorityQueue<TElement,
|
|
|
|
|
TPriority>` (.NET 6+) is well-optimized and avoids maintenance burden.
|
2026-03-24 10:02:54 +00:00
|
|
|
|
|
|
|
|
### ConcurrentDictionary sizing
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Pre-size `World.chunks` to avoid rehashing:
|
2026-03-24 10:02:54 +00:00
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
new ConcurrentDictionary<(int, int), ChunkColumn>(
|
|
|
|
|
concurrencyLevel: Environment.ProcessorCount, capacity: 1024);
|
2026-03-24 10:02:54 +00:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Threading
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Minimize lock scope
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Copy data out under the lock, process outside:
|
2026-03-24 10:02:54 +00:00
|
|
|
```csharp
|
|
|
|
|
List<Item> snapshot;
|
2026-03-24 10:17:52 +00:00
|
|
|
lock (_lock) { snapshot = [.. _items]; }
|
|
|
|
|
foreach (var item in snapshot) ExpensiveProcess(item);
|
2026-03-24 10:02:54 +00:00
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Batch InvokeOnMainThread
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Each `InvokeOnMainThread()` call blocks until the main thread runs it.
|
|
|
|
|
In loops, batch into a single call:
|
2026-03-24 10:02:54 +00:00
|
|
|
```csharp
|
|
|
|
|
handler.InvokeOnMainThread(() =>
|
|
|
|
|
{
|
2026-03-24 10:17:52 +00:00
|
|
|
foreach (var entity in entities) UpdateEntity(entity);
|
2026-03-24 10:02:54 +00:00
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
### Channel\<T\> over BlockingCollection\<T\>
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
Lower overhead, async-friendly:
|
2026-03-24 10:02:54 +00:00
|
|
|
```csharp
|
2026-03-24 10:17:52 +00:00
|
|
|
var ch = Channel.CreateUnbounded<(int Id, Memory<byte> Data)>(
|
2026-03-24 10:02:54 +00:00
|
|
|
new UnboundedChannelOptions { SingleReader = true });
|
2026-03-24 10:17:52 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Common Optimization Anti-Patterns
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
These are things agents (and humans) rationalize doing. Every one of them
|
|
|
|
|
makes performance worse or wastes effort.
|
|
|
|
|
|
|
|
|
|
| Anti-pattern | Why it's wrong |
|
|
|
|
|
|---|---|
|
|
|
|
|
| Adding `AggressiveInlining` to large methods | Bloats call sites, causes more cache misses, makes code *slower* |
|
|
|
|
|
| Optimizing login/config code | Runs once per session; clarity matters more than speed |
|
|
|
|
|
| Using `ConcurrentDictionary` where a plain `Dictionary` + lock suffices | Concurrent overhead on uncontested paths costs more than a lock |
|
|
|
|
|
| Replacing LINQ with manual loops on cold paths | No measurable gain, worse readability |
|
|
|
|
|
| Caching mutable state to avoid re-reads | Stale cache bugs are harder to diagnose than the perf hit |
|
|
|
|
|
| `Task.Result` / `.Wait()` on hot paths | Deadlock risk and thread-pool starvation |
|
|
|
|
|
|
|
|
|
|
---
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
## Pre-Commit Checklist
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
ALWAYS verify before submitting a performance change:
|
2026-03-24 10:02:54 +00:00
|
|
|
|
2026-03-24 10:17:52 +00:00
|
|
|
- [ ] Hot path identified with profiling data, not guesswork
|
|
|
|
|
- [ ] Before/after measurements recorded (allocation count, throughput, or latency)
|
2026-03-24 10:02:54 +00:00
|
|
|
- [ ] No new allocations inside per-tick or per-packet methods
|
2026-03-24 10:17:52 +00:00
|
|
|
- [ ] `[MethodImpl]` attributes match method call frequency and IL size
|
|
|
|
|
- [ ] Frozen collections used for any static lookup table
|
|
|
|
|
- [ ] Lock scopes contain no I/O or expensive work
|
2026-03-24 10:02:54 +00:00
|
|
|
- [ ] No `Task.Result`, `.Wait()`, or `GetAwaiter().GetResult()` on hot paths
|
2026-03-24 10:17:52 +00:00
|
|
|
- [ ] Thread safety preserved (checked existing lock/concurrent patterns)
|
|
|
|
|
- [ ] Optimization comments explain non-obvious choices
|
|
|
|
|
- [ ] Code still compiles and passes all existing checks
|