diff --git a/changelog.d/9181-uint8array-fixed-size-allocation-keeps-its-view.md b/changelog.d/9181-uint8array-fixed-size-allocation-keeps-its-view.md new file mode 100644 index 0000000000..e6cbba4152 --- /dev/null +++ b/changelog.d/9181-uint8array-fixed-size-allocation-keeps-its-view.md @@ -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. diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index b7503ec070..a64a722739 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -991,6 +991,27 @@ fn is_owned_u8_buffer_alloc(expr: &Expr, known_length_locals: &HashSet) -> fn is_fresh_uint8array_length_expr(expr: &Expr, known_length_locals: &HashSet) -> 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), } }