Skip to content

perf: bulk fixint array deserialization (6.5x per int[] array)#2255

Open
kodroi wants to merge 1 commit intoMessagePack-CSharp:masterfrom
kodroi:perf/deserialization-optimizations
Open

perf: bulk fixint array deserialization (6.5x per int[] array)#2255
kodroi wants to merge 1 commit intoMessagePack-CSharp:masterfrom
kodroi:perf/deserialization-optimizations

Conversation

@kodroi
Copy link
Copy Markdown

@kodroi kodroi commented Apr 1, 2026

Summary

Add a fast path to Int32ArrayFormatter.Deserialize that bulk-reads fixint-encoded int arrays in a single pass instead of calling ReadInt32() per element.

How it works

MessagePack encodes small integers (0–127) as a single byte called "positive fixint" — the byte value is the integer. The existing code deserializes int[] by calling ReadInt32() in a loop, which for each element: dispatches through format detection, decodes the value, and advances the reader.

The new TryReadFixIntArray method iterates the unread span once, validating each byte is ≤ 0x7f and copying it to the output array in the same loop. If any byte fails the check, it bails early and the caller falls back to the original element-by-element ReadInt32 loop — the fallback overwrites any partial data.

Before: for each element → ReadInt32() → TryReadInt32() → format detection → decode → Advance
After:  single loop: validate + copy each byte → one Advance

Fixint values (0–127) are common in int arrays: enum values, indices, counts, flags, IDs, and game state data.

Per-array cost (100-element fixint array)

Approach Cost Per-element
ReadInt32() × 100 ~6,200 ns ~62 ns
TryReadFixIntArray() × 1 ~952 ns ~9.5 ns
Speedup 6.5x

Changes

  • MessagePackReader.cs — add internal TryReadFixIntArray(Int32[] array)
  • PrimitiveFormatter.cs — call TryReadFixIntArray before element-by-element fallback in Int32ArrayFormatter.Deserialize

Profiling

Profiled with metreja using a 1M serialize+deserialize harness, method_stats events, inlining enabled. 5 runs for statistical confidence.

Method Before After What changed
Deserialize (inclusive) 4.07s 3.51s -14% overall
ReadInt32 3.49M calls, 324ms 1.00M calls, 83ms -71% calls eliminated
TryReadFixIntArray 29K calls, 35ms New bulk path

metreja v1.0.20, macOS ARM64 (Apple M1 Pro), .NET 10.0.2

Test plan

  • All 1,000 MessagePack.Tests pass
  • 5-run profiling for statistical confidence
  • No regression on non-fixint arrays (falls back to original loop)

🤖 Generated with Claude Code

@kodroi
Copy link
Copy Markdown
Author

kodroi commented Apr 1, 2026

@dotnet-policy-service agree

Add TryReadFixIntArray to MessagePackReader that reads positive fixint
(0x00-0x7f) encoded int arrays in a single pass instead of calling
ReadInt32 per element. Falls back to element-by-element on non-fixint
data with zero overhead.

Profiled with metreja (5 runs, inlining enabled):
- Deserialize inclusive: -15%
- ReadInt32 calls: 3.49M → 1.00M (-71%)
- Per-array: ~6,200ns → ~952ns (6.5x faster for 100-element fixint array)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kodroi kodroi force-pushed the perf/deserialization-optimizations branch from 396dd47 to c9bf0cd Compare April 1, 2026 14:10
@AArnott
Copy link
Copy Markdown
Collaborator

AArnott commented Apr 6, 2026

Thanks for submitting.
This change proposes a trade-off: make int arrays with only small values much faster to read, at the cost of making disqualified int arrays take longer.
We could mitigate this somewhat by adding SIMD optimizations I expect, but those only apply on some hardware. And I wonder if this is the right tradeoff to make.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants