Repository navigation
perf: reduce per-value work in unsigned int and header length decoding - #124
Merged
Merged
Conversation
…header length reads Profile (flame heap+CPU) of decode shows unsigned-int decoding runs a Math.pow per byte, and the 3-byte header length read allocates a temporary slice. decodeUnsignedInt now dispatches to the fixed-width readUInt8/ readUInt16BE/readUInt32BE (exact for sizes 1/2/4, matching decodeSigned), and readUIntBE computes the 3-byte value inline instead of slicing. Behavior-preserving; the 3-byte read produces the same value as the sliced readUIntBE, and fixed-width readers are exact (<= 2^32). All 4391 tests pass on Node 22/24/26.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
This round was driven by
@platformatic/flame's heap profiler (which also runs its CPU profiler) to look for allocation/GC benefits.The heap profiles showed that decode is not allocation-bound: on every dataset — including the "primitive-heavy"
mixed.json— nativeutf8Slicedominates (46–55% self), because object keys are strings and each is materialized as a JS string. Young-gen GC totals ~0.5–1.5% of runtime (all scavenges, no old-gen collections), so there is little headroom from reducing allocation.Two concrete, safe wins did surface and are landed here:
decodeUnsignedInt(uint8/16/32) spun a loop callingbuf.readUInt8(...)andMath.pow(256, ...)per byte. It now dispatches to the fixed-widthreadUInt8/readUInt16BE/readUInt32BE, exactly mirroringdecodeSigned. Results are exact (≤ 2³²), and size 8 keeps the existing path unchanged.readUIntBEextracted a temporary slice for the 3-byte (ext16length) header read. It now computes the value inline, avoiding that transient allocation.Testing
standardlint clean.sample-large/example4/mixed): ratios ≈ 1.016–1.033, i.e. neutral-to-modestly positive and within the noisy machine's error band.Honest note on the rest of the round
I also tried frame object pooling in the iterative decoder (the heap profile's top application allocation site, ~43 frames/decode). It regressed decode by ~11–14%: the per-frame field-set on
openArrayFrame/openMapFrameand the field-reset infreeFramecost more than the (tiny) young-gen allocations they reclaimed. That change was reverted, not included.The takeaway: on these string-heavy datasets msgpack5's decode is bounded by native UTF-8 string materialization and the
tryDecodeJS dispatch, not by allocation/GC. Closing the remaining gap to msgpackr is a native-acceleration problem rather than an allocation problem.