perf(runtime): #92 BigInt arena alloc + BigInt(str) === value comparison - #172
Merged
Merged
Conversation
proggeramlug
force-pushed
the
fix-92-bigint-small-int-fast-path
branch
from
April 24, 2026 05:07
527a2ca to
1ee5140
Compare
Closes #92 (BigInt half) via PR #172. Two separate problems flagged in #92. ## 1. Allocation bottleneck `BigInt('123456789012345')` measured 15 ms / 100k iterations (3× Node). `bigint_alloc()` called `gc_malloc` which costs ~100–150 ns per call: system `malloc` + `MALLOC_STATE` Vec push + HashSet insert. For 100k BigInts that's ~15 ms. Switched `bigint_alloc()` from `gc_malloc` to `arena_alloc_gc` (bump-pointer). The GC already handles arena BigInts correctly: - `try_mark_value` (`gc.rs`) extracts `BIGINT_TAG` pointers and validates them against `ValidPointerSet`, which includes all arena objects. - `drain_trace_worklist` has a `GC_TYPE_STRING | GC_TYPE_BIGINT => {}` arm (BigInts have no child JSValue refs to trace). - Sweep resets empty arena blocks; dead BigInts in all-dead blocks are reclaimed without per-object `dealloc`. All `bigint_alloc()` callers (`js_bigint_from_i64`, `_from_u64`, `_from_string`, `_neg`, `_add`, ...) already fully initialize the limb array before returning, so the unzeroed arena memory is safe. ## 2. `BigInt(str) === BigInt(str)` returned false `is_bigint_expr` checks `ctx.local_types[id] == HirType::BigInt`, but `const a = BigInt('5')` arrived with `ty: Any` in HIR because no code refined the type for `BigIntCoerce` expressions (only `Expr::BigInt(_)` literals like `5n` were handled). Without the BigInt fast-path, `===` fell through to f64 bit comparison (pointer equality) → two distinct heap objects → always `false`. Added `Expr::BigIntCoerce(_)` arm to: - `crates/perry-codegen/src/type_analysis.rs::refine_type_from_init` - `crates/perry-codegen/src/boxed_vars.rs` So `const x = BigInt(str)` now binds with `Type::BigInt` and hits the BigInt-specific `===` path. The string-parsing fast path (`i64::from_str` in `js_bigint_from_string`) was already present in the codebase and is untouched here. ## Maintainer fixup Removed `bench_bigint_92.ts` from the repo root at merge — it was a micro-benchmark scratchpad for issue #92 measurements, same family as the 17 stale `bench_*.ts` scratch files cleaned up in v0.5.163. Cloud-authored PR, manually audited and metadata (version bump + CLAUDE.md entry + bench scratch cleanup) folded in at merge.
proggeramlug
force-pushed
the
fix-92-bigint-small-int-fast-path
branch
from
April 24, 2026 05:41
1ee5140 to
5dfbd3d
Compare
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.
Closes #92.
Root cause analysis
Issue #92 measured
BigInt('123456789012345')at 15 ms / 100k iterations (3× Node). Two separate problems:1. Allocation bottleneck —
bigint_alloc()calledgc_mallocwhich costs ~100–150 ns per call: systemmalloc+MALLOC_STATEVec push + HashSet insert. For 100k BigInts that's ~15 ms.2.
BigInt(str) === BigInt(str)returned false —is_bigint_exprchecksctx.local_types[id] == HirType::BigInt, butconst a = BigInt('5')arrived withty: Anyin the HIR and no code refined it forBigIntCoerceexpressions. Without the BigInt fast path,===fell through to f64 bit comparison (pointer equality) → two distinct heap objects → alwaysfalse.Note: the string-parsing fast path (
i64::from_strinjs_bigint_from_string) was already present in the codebase; this PR addresses the remaining allocation and correctness problems.Changes
crates/perry-runtime/src/bigint.rsSwitch
bigint_alloc()fromgc_malloctoarena_alloc_gc(bump-pointer).The GC already handles arena BigInts correctly:
try_mark_valueingc.rsextractsBIGINT_TAGpointers and validates them againstValidPointerSet, which includes all arena objects.drain_trace_worklisthas aGC_TYPE_STRING | GC_TYPE_BIGINT => {}arm (BigInts have no child JSValue refs to trace).dealloc.All
bigint_alloc()callers (js_bigint_from_i64,js_bigint_from_u64,js_bigint_from_string,js_bigint_neg,js_bigint_add, etc.) already fully initialize the limb array before returning, so the unzeroed arena memory is safe.crates/perry-codegen/src/type_analysis.rsAdd
Expr::BigIntCoerce(_)torefine_type_from_init. Previously onlyExpr::BigInt(_)(BigInt literals like5n) was handled, soconst x = BigInt(str)stayed typed asAny.crates/perry-codegen/src/boxed_vars.rsSame fix in
refine_type_from_init_simple(the lighter version used by the closure-capture type-analysis path).crates/perry-codegen/src/codegen.rscompile_module_entrycreated the module-initFnCtxwithlocal_types: HashMap::new()(empty). Regular functionFnCtxs receivemodule_local_typesentries at line 1712. Addingcollect_let_types_in_stmts(&hir.init, ...)before the FnCtx construction gives module-levelBigInt(...)locals the same type-aware dispatch as function-local ones.bench_bigint_92.tsMicro-benchmark covering i64-range and large-decimal paths, plus the cross-path equality/arithmetic correctness checks required by the issue.
Benchmark results
BigInt('123456789012345')× 100kBigInt('99999999999999999999999999999')× 100kBigInt('5') === BigInt('5')BigInt('5') === 5nBigInt('5') + BigInt('3')BigInt('10') * BigInt('10')Perry is now 40% faster than Node for i64-range BigInt strings. The large-decimal path is a separate pre-existing issue (multi-limb school multiplication, ~6× slower than Node's optimised bignums) — out of scope for this PR.
Test results
cargo test --release -p perry-runtime --lib: 111/111 passtest_gap_bigint.ts: byte-for-byte match with Nodeasync_advanced,console_methods,string_methods,typed_arrays— none BigInt-related)Generated by Claude Code