Skip to content

perf(runtime): #92 BigInt arena alloc + BigInt(str) === value comparison - #172

Merged
proggeramlug merged 1 commit into
mainfrom
fix-92-bigint-small-int-fast-path
Apr 24, 2026
Merged

perf(runtime): #92 BigInt arena alloc + BigInt(str) === value comparison#172
proggeramlug merged 1 commit into
mainfrom
fix-92-bigint-small-int-fast-path

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes #92.

Root cause analysis

Issue #92 measured BigInt('123456789012345') at 15 ms / 100k iterations (3× Node). Two separate problems:

1. Allocation bottleneckbigint_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.

2. BigInt(str) === BigInt(str) returned falseis_bigint_expr checks ctx.local_types[id] == HirType::BigInt, but const a = BigInt('5') arrived with ty: Any in the HIR and no code refined it for BigIntCoerce expressions. Without the BigInt fast path, === fell through to f64 bit comparison (pointer equality) → two distinct heap objects → always false.

Note: the string-parsing fast path (i64::from_str in js_bigint_from_string) was already present in the codebase; this PR addresses the remaining allocation and correctness problems.

Changes

crates/perry-runtime/src/bigint.rs

Switch bigint_alloc() from gc_malloc to arena_alloc_gc (bump-pointer).

The GC already handles arena BigInts correctly:

  • try_mark_value in 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, 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.rs

Add Expr::BigIntCoerce(_) to refine_type_from_init. Previously only Expr::BigInt(_) (BigInt literals like 5n) was handled, so const x = BigInt(str) stayed typed as Any.

crates/perry-codegen/src/boxed_vars.rs

Same fix in refine_type_from_init_simple (the lighter version used by the closure-capture type-analysis path).

crates/perry-codegen/src/codegen.rs

compile_module_entry created the module-init FnCtx with local_types: HashMap::new() (empty). Regular function FnCtxs receive module_local_types entries at line 1712. Adding collect_let_types_in_stmts(&hir.init, ...) before the FnCtx construction gives module-level BigInt(...) locals the same type-aware dispatch as function-local ones.

bench_bigint_92.ts

Micro-benchmark covering i64-range and large-decimal paths, plus the cross-path equality/arithmetic correctness checks required by the issue.

Benchmark results

Perry (this PR) Node.js
BigInt('123456789012345') × 100k 6 ms 10 ms
BigInt('99999999999999999999999999999') × 100k 49 ms 8 ms
BigInt('5') === BigInt('5') true true
BigInt('5') === 5n true true
BigInt('5') + BigInt('3') 8 8
BigInt('10') * BigInt('10') 100 100

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 pass
  • test_gap_bigint.ts: byte-for-byte match with Node
  • Full gap suite: 24/28 pass (4 pre-existing failures: async_advanced, console_methods, string_methods, typed_arrays — none BigInt-related)

Generated by Claude Code

@proggeramlug
proggeramlug force-pushed the fix-92-bigint-small-int-fast-path branch from 527a2ca to 1ee5140 Compare April 24, 2026 05:07
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.
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.

Bulk decode throughput: 2-3× slower than V8/JSC on primitives used in the Postgres driver hot path

1 participant