perf: chunked digit parsing/formatting, cheaper ToBigInt and rounding halves#45
Merged
quagmt merged 1 commit intoJun 12, 2026
Conversation
… halves Four small, behavior-preserving optimizations on hot paths: - bint.go: digitToU128 parses long digit strings in chunks of up to 19 digits using uint64 arithmetic, requiring only one u128 Mul64+Add64 per chunk instead of per digit (~-37% on Parse of long strings). - codec.go: appendBuffer reduces the u128 integer part to uint64 chunks via at most two QuoRem64(1e19) calls and formats each chunk with 64-bit arithmetic only, instead of one 128-bit division per 2 digits (-16% to -22% on String/MarshalJSON). - u128.go: ToBigInt sets big.Int words directly via SetBits on 64-bit platforms, avoiding the intermediate byte buffer and its decoding (-23% time, -40% bytes allocated). The portable byte-buffer path is kept for 32-bit platforms. - decimal.go: RoundHAZ/RoundHTZ compute half of the rounding factor with a single shift instead of a 128-bit division, since the factor is always a power of 10 with hi == 0 (-24% to -28%); RoundBank already used the cheap form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #45 +/- ##
==========================================
- Coverage 96.89% 96.67% -0.22%
==========================================
Files 5 5
Lines 1481 1506 +25
==========================================
+ Hits 1435 1456 +21
- Misses 23 27 +4
Partials 23 23 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a set of low-level, hot-path optimizations in core decimal parsing/formatting and rounding logic, plus a faster u128 -> big.Int conversion, aiming to reduce allocations and expensive 128-bit operations without changing externally observable behavior.
Changes:
- Parse long digit sequences into
u128using 19-digituint64chunks to reduce per-digitu128mul/add work. - Format
u128integer parts by reducing to 19-digituint64chunks and emitting digits using only 64-bit arithmetic. - Optimize
u128.ToBigInt()on 64-bit platforms viabig.Int.SetBits, avoiding the intermediate byte buffer and decoding. - Compute
factor/2inRoundHAZ/RoundHTZusing a cheap shift based on established constraints forfactor.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
bint.go |
Implements chunked (≤19 digits) parsing in digitToU128 to reduce u128 arithmetic in long-number parsing paths. |
codec.go |
Replaces per-2-digit u128 division formatting with appendU128 chunking to reduce u128 divisions and overhead during string/JSON formatting. |
u128.go |
Adds a 64-bit fast path for ToBigInt() using big.Int.SetBits to cut allocations and conversion overhead. |
decimal.go |
Optimizes half-threshold computation in RoundHAZ/RoundHTZ by shifting a known-even pow10 factor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
This PR contains four small, behavior-preserving optimizations on hot paths. No API or behavior changes — all existing tests, fuzz targets, and lint checks pass unchanged.
Changes
1.
bint.go— chunked digit parsing indigitToU128Long digit strings (>19 digits) are now parsed in chunks of up to 19 digits. Each chunk is accumulated with plain
uint64arithmetic, so only one u128Mul64+Add64is needed per chunk instead of per digit. ~-37% onParseof long strings that still fit in u128.2.
codec.go— chunked integer-part formatting inappendBufferThe u128 integer part is first reduced to
uint64chunks of 19 digits via at most twoQuoRem64(1e19)calls, then each chunk is formatted using 64-bit arithmetic only, instead of one 128-bit division per 2 digits. This also removes the per-2-digitquoRem64helper call overhead for values withhi == 0. -16% to -22% onString/MarshalJSON. (The now-unusedquoRem64helper is removed.)3.
u128.go—ToBigIntviabig.Int.SetBitsOn 64-bit platforms the words are set directly with
SetBits, avoiding the intermediate 16-byte buffer, the big-endian encoding, andSetBytesdecoding (-23% time, -40% bytes allocated). The portable byte-buffer path is kept for 32-bit platforms (wherebig.Wordis 32 bits); the branch is onbits.UintSize, a compile-time constant, so it is eliminated by the compiler. This helps every big.Int fallback path (GetBig, div/round/quo fallbacks, etc.).4.
decimal.go— cheaper half computation inRoundHAZ/RoundHTZfactoris alwayspow10[d.prec-prec]with1 <= d.prec-prec <= 19, i.e. a power of 10 withhi == 0, sofactor / 2is a single shift onfactor.loinstead of a 128-bitQuoRem64(2).RoundBankalready used the cheap form. -24% to -28% onRoundHAZ/RoundHTZ.Benchmarks
benchstatover upstream'sbenchmarks/suite (udec cases),-benchtime=0.2s -count=5, Apple M1 Pro, go1.26.1:Micro-benchmarks for the paths without upstream benchmarks (same settings):
Remaining benchmarks (Add/Sub/Mul/Div/Pow, binary codecs, UnmarshalJSON, StringFallBack) are unchanged within noise.
Validation
go test -tags='!fuzz' -race -failfast ./...passesgo test -tags=fuzz -fuzz=FuzzParseand-fuzz=FuzzMarshalJSONrun clean for 20s each (~2.3M execs each)go vet ./...,gofmt, andgolangci-lint runclean on all changed lines (the 3 remaining repo findings pre-date this PR and are on untouched lines)replacedirective — passes🤖 Generated with Claude Code