Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
`new Uint8Array(SIZE * SIZE)` now keeps the buffer-view tier that
`new Uint8Array(SIZE)` already had.

`is_fresh_uint8array_length_expr` accepted a literal or a known-length local, but
nothing built from them, so an allocation whose size was an arithmetic expression
was never recognised as a freshly allocated owned buffer. Without that
classification the receiver gets no view at all, and with no view every element
read falls back to a runtime call — however well its index is proven, and however
inline the arithmetic around it already is.

That is why `benchmarks/suite/bench_int_arithmetic.ts` still paid 54
`js_uint8array_index_get_value` calls per pixel after the interval bounds proof
landed: not the index, the allocation. Changing only that benchmark's allocation
to a literal length, with nothing else touched, is what isolated it.

The predicate asks whether the allocation's size is FIXED, never what it is —
`length_source_from_expr` resolves the value later, with a `FnCtx` in hand, and
records no constant length when it cannot. A sum, difference or product of the
same leaves is therefore exactly as fixed as either leaf alone, and qualifies on
the same argument.

Measured on an idle Mac mini against this change's exact merge base, both
binaries built in one run, interleaved, min of five, self-timed:
`bench_int_arithmetic` 462 → 149 ms with Node at 62 — 7.5× Node down to 2.4×,
identical checksums.

Verified byte-identical to Node on the two differentials from the parent change
(eight cases around the byte read, seven around the bounds proof), plus the
probe whose interval genuinely exceeds its buffer, which must and does still
decline to the checked call rather than reading out of bounds.
21 changes: 21 additions & 0 deletions crates/perry-codegen/src/collectors/hir_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,27 @@ fn is_owned_u8_buffer_alloc(expr: &Expr, known_length_locals: &HashSet<u32>) ->
fn is_fresh_uint8array_length_expr(expr: &Expr, known_length_locals: &HashSet<u32>) -> bool {
match expr {
Expr::LocalGet(id) => known_length_locals.contains(id),
// `new Uint8Array(SIZE * SIZE)` — a fixed arithmetic combination of
// literals and known-length locals is exactly as fixed as either leaf
// on its own, and this predicate asks only whether the allocation's
// SIZE is fixed, never what it is (`length_source_from_expr` resolves
// the value later, with a `FnCtx` in hand, and simply records no
// constant length when it cannot).
//
// Rejecting the product cost the whole buffer-view tier for the
// receiver: no view means no inline element load, so every read paid a
// `js_uint8array_index_get_value` call —
// `benchmarks/suite/bench_int_arithmetic.ts` allocates exactly this way
// and spent 54 calls per pixel because of it.
Expr::Binary { op, left, right }
if matches!(
op,
perry_hir::BinaryOp::Add | perry_hir::BinaryOp::Sub | perry_hir::BinaryOp::Mul
) =>
{
is_fresh_uint8array_length_expr(left, known_length_locals)
&& is_fresh_uint8array_length_expr(right, known_length_locals)
}
_ => is_fresh_uint8array_length_literal(expr),
}
}
Expand Down
Loading