From a6f6addb87565148900b58f036924feea1a83ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 30 Aug 2026 13:13:25 +0200 Subject: [PATCH 1/2] perf(codegen): a fixed-size allocation expression keeps the buffer-view tier (bench_int_arithmetic 395 -> 149 ms) Follow-up to #9146, which left this on the table. is_fresh_uint8array_length_expr accepted a literal or a known-length local but nothing built from them, so `new Uint8Array(SIZE * SIZE)` was never classified as a freshly allocated owned buffer. With no classification the receiver gets no buffer view, and with no view every element read falls back to js_uint8array_index_get_value no matter how well the index is proven -- which is why bench_int_arithmetic still paid 54 calls per pixel after #9146's interval bounds proof. Changing only that benchmark's allocation to a literal length was what isolated it: 395 -> 168 ms with the calls gone. 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, and records no constant length when it cannot), so a sum, difference or product of literals and known-length locals qualifies on exactly the same argument as either leaf. Mac mini, against this change's exact merge base, both binaries built in one run, interleaved, min of 5, self-timed: bench_int_arithmetic 462 -> 149 ms (node 62), 7.5x node down to 2.4x, identical checksums. Both #9146 differentials stay byte-identical to node, and the probe whose interval genuinely exceeds its buffer still declines to the checked call. Claude-Session: https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT --- ...ay-fixed-size-allocation-keeps-its-view.md | 30 +++++++++++++++++++ .../perry-codegen/src/collectors/hir_facts.rs | 21 +++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 changelog.d/uint8array-fixed-size-allocation-keeps-its-view.md diff --git a/changelog.d/uint8array-fixed-size-allocation-keeps-its-view.md b/changelog.d/uint8array-fixed-size-allocation-keeps-its-view.md new file mode 100644 index 0000000000..e6cbba4152 --- /dev/null +++ b/changelog.d/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), } } From 5cde4aa4923929ae93ab0ab511c75ff99dfd1416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 30 Aug 2026 18:09:09 +0200 Subject: [PATCH 2/2] chore: PR-key the changelog fragment --- ...md => 9181-uint8array-fixed-size-allocation-keeps-its-view.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{uint8array-fixed-size-allocation-keeps-its-view.md => 9181-uint8array-fixed-size-allocation-keeps-its-view.md} (100%) diff --git a/changelog.d/uint8array-fixed-size-allocation-keeps-its-view.md b/changelog.d/9181-uint8array-fixed-size-allocation-keeps-its-view.md similarity index 100% rename from changelog.d/uint8array-fixed-size-allocation-keeps-its-view.md rename to changelog.d/9181-uint8array-fixed-size-allocation-keeps-its-view.md