Improve integer read performance - #19
Merged
landabaso merged 3 commits intoSep 7, 2026
Merged
Conversation
junderw
reviewed
Sep 7, 2026
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.
Summary
BufferandUint8Arrayinputs.Bufferallocations.Why
The Node.js entry point called
Buffer.from(buffer)before every integer read.Buffer.from(Uint8Array)copies the complete input even when a reader needs only 1, 2, 4 or 8 bytes. The cost of reading a fixed-width integer therefore grew with the size of the input. Each read also created a temporary allocation.Integer readers now access only the required bytes. Their work is constant for each integer width and they do not allocate a temporary
Buffer.Implementation
The 16-bit and 32-bit readers use the same positional arithmetic approach as Node.js
Buffer. Multiplication preserves the full unsigned 32-bit range without relying on signed bitwise coercion.The 64-bit readers first compose exact
loandhi32-bit numbers. They then convert only those two values toBigInt. This avoids converting and shifting each byte as a separateBigIntoperation.readInt8andreadInt16use an explicit two's complement conversion. This is clearer than the branchless sign-extension expression used by Node.jsBuffer. The wider signed readers use a signed high part, following the same approach as Node.jsBuffer.Direct
Uint8Arrayindexing does not validate offsets likeBuffer.read*does. A shared check rejects invalid and out-of-range offsets before access. The browser entry point is unchanged.Performance
The main improvement comes from removing a full-array copy from each read. A local microbenchmark illustrates how that cost changes with input size. It compares
readUInt32(bytes, offset, "LE")with the previousBuffer.from(bytes).readUInt32LE(offset)path.Measurements were collected on arm64 macOS with Node.js
v20.19.4and the built ESM entry point. Both paths were warmed up, iteration counts were calibrated per path and each result is the median of seven timed runs.The smaller inputs are closer to hashes, scripts and transaction-sized data seen in common Bitcoin operations. The 512 KiB input is included only as a stress case to show that the previous cost grows with the full input while the direct read stays nearly constant.
The decoding formulas were also compared with small local checks:
readUInt64and 2.57x faster forreadInt64than shifting eightBigIntbyte values.Bufferform and was easier to read.These local measurements are directional evidence, not a performance guarantee or a maintained benchmark. The new readers remove the O(n) full-input copy and access only a fixed number of bytes, so their work no longer scales with input size. The decoding logic closely follows Node.js
Buffer, but operates directly on the original input. This is expected to improve performance because the previous path performed the same fixed-width decoding after copying the full input. The benefit grows and becomes more visible as the input size increases. A maintained benchmark suite is outside the scope of this PR.TypeScript Build
When
0.0.9was published, clean installs resolved type definitions that worked with TypeScript4.4.4. Without a lockfile, Jest can now resolve newer definitions that require TypeScript5.1, which the existing compiler cannot parse. The compiler now loads only the Node.js and Jest ambient types this project needs, excluding unrelated definitions from the build.Verification
Bufferacross 2,000 deterministic byte vectors each.npm pack --dry-runincludes the expected eight package files.