From ba80270d38b21ca6364fc47068c06ba48b669ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 4 Sep 2026 08:25:25 +0000 Subject: [PATCH] test(codegen): pin matrix multiply affine fast path (#9248) --- ...affine-window-hoist-and-accumulator-set.md | 6 +- .../tests/issue_9253_affine_range_index.rs | 87 ++++++++++++++++--- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/changelog.d/affine-window-hoist-and-accumulator-set.md b/changelog.d/affine-window-hoist-and-accumulator-set.md index eab1c7622c..d570ca43eb 100644 --- a/changelog.d/affine-window-hoist-and-accumulator-set.md +++ b/changelog.d/affine-window-hoist-and-accumulator-set.md @@ -1,6 +1,6 @@ -**`16_matrix_multiply` is now twice as fast as node** (100 ms → 16 ms against -node's 32 on an idle machine; checksums identical) — the last benchmark in -the suite crosses parity, via two changes and one guard fix. +**Fixes #9248: `16_matrix_multiply` is now twice as fast as node** (100 ms → +16 ms against node's 32 on an idle machine; checksums identical) — the last +benchmark in the suite crosses parity, via two changes and one guard fix. **The affine window is proven at the loop's endpoints.** An index tree linear in the counter takes its extremes at the interval's ends, so the entry guard diff --git a/crates/perry/tests/issue_9253_affine_range_index.rs b/crates/perry/tests/issue_9253_affine_range_index.rs index 8c80686436..fec29019e8 100644 --- a/crates/perry/tests/issue_9253_affine_range_index.rs +++ b/crates/perry/tests/issue_9253_affine_range_index.rs @@ -1,6 +1,7 @@ -//! Regression coverage for #9253: a counted loop whose reads use an AFFINE -//! index — `a[i * size + k]` — now hoists its receiver guard into the -//! preheader instead of re-deriving it on every access. +//! Regression coverage for #9248 and its affine-index implementation #9253: +//! a counted loop whose reads use AFFINE indices — `a[i * size + k]` and +//! `b[k * size + j]` — guards both receivers in the preheader and keeps the +//! loop-carried accumulator native. //! //! `16_matrix_multiply`'s inner loop spends 97% of its time inside generated //! code with no runtime calls, so the cost was never a missed inlining. Per @@ -13,13 +14,15 @@ //! motion barrier. //! //! The packed-f64 RANGE tier already had everything needed except the index -//! shape — a loop-invariant local/parameter bound, N-array guard emission, and -//! GC-safe receiver caching refreshed at the back-edge poll. This adds the +//! shape and multi-array accumulator admission — a loop-invariant +//! local/parameter bound, N-array guard emission, and GC-safe receiver caching +//! refreshed at the back-edge poll. This adds the //! affine index: the entry guard proves the RECEIVER once, and each read pays //! one inline `icmp ult idx, len` with a side exit. //! -//! Measured on an idle Mac mini, self-timed min of 7: 100 ms -> 69 ms against -//! node's 33 ms (3.03x -> 2.09x). +//! Endpoint window hoisting and admitting the guarded array set for the +//! accumulator then reduced the benchmark from 69 ms to 16 ms against node's +//! 32 ms (the original baseline was 100 ms). use std::path::{Path, PathBuf}; use std::process::{Command, Output}; @@ -64,6 +67,32 @@ fn ir(stderr: &str) -> String { std::fs::read_to_string(path).expect("read kept LLVM IR") } +fn specialized_function_ir<'a>(text: &'a str, function: &str) -> &'a str { + let marker = format!("__{function}$spec"); + let marker_at = text + .find(&marker) + .unwrap_or_else(|| panic!("missing specialized function `{function}`")); + let start = text[..marker_at] + .rfind("define ") + .unwrap_or_else(|| panic!("missing definition for specialized function `{function}`")); + let rest = &text[start..]; + let end = rest + .find("\n}\n") + .unwrap_or_else(|| panic!("unterminated specialized function `{function}`")); + &rest[..end + 2] +} + +fn named_block<'a>(function_ir: &'a str, prefix: &str) -> &'a str { + let marker = format!("\n{prefix}"); + let start = function_ir + .find(&marker) + .unwrap_or_else(|| panic!("missing block `{prefix}`")) + + 1; + let rest = &function_ir[start..]; + let end = rest.find("\n\n").unwrap_or(rest.len()); + &rest[..end] +} + fn run(bin: &Path, dir: &Path, moving_gc: bool) -> Output { let mut command = Command::new(bin); command.current_dir(dir); @@ -101,10 +130,11 @@ for (let i = 0; i < 64; i++) { a.push(i % 7); b.push(i % 5); } console.log("matmul:" + matmul(a, b, 8)); "#; -/// The affine index shape reaches the clone at all. Without this the guard is -/// re-derived per access for both receivers, which is the whole issue. +/// Both affine receiver guards reach the clone and the accumulator remains a +/// native double. Losing either property restores #9248's per-read guard or +/// per-iteration dynamic-add cost. #[test] -fn an_affine_index_admits_the_range_clone_and_agrees_with_the_generic_path() { +fn matmul_guards_both_affine_arrays_and_unboxes_the_accumulator() { let dir = tempfile::tempdir().expect("tempdir"); let (bin, stderr) = compile(dir.path(), MATMUL); let text = ir(&stderr); @@ -114,12 +144,41 @@ fn an_affine_index_admits_the_range_clone_and_agrees_with_the_generic_path() { // detector looked for the per-read `packed_f64_affine.index_fits` block, // which the window-hoist legitimately removed: a window proven at the // loop's endpoints leaves the read as a bare trunc + raw load with no - // named block at all. + // named block at all. Count CALLS (not the declaration): one must guard + // `a` and one must guard `b`. + let matmul_ir = specialized_function_ir(&text, "matmul"); + assert_eq!( + matmul_ir + .matches("call i32 @js_typed_feedback_packed_f64_array_loop_guard") + .count(), + 2, + "#9248: both affine array receivers must be guarded once in the preheader" + ); + + // The fast body's two raw loads feed one native multiply and add. In + // particular, the loop must not regain either generic index helpers or + // the boxed accumulator's dynamic add / write-barrier branch. + let fast_body = named_block(matmul_ir, "for.packed_f64_range_fast.body."); assert!( - text.contains("js_typed_feedback_packed_f64_array_loop_guard"), - "#9253: `a[i * size + k]` must reach the affine tier; without it \ - the receiver guard re-executes per access" + fast_body.matches("load double, ptr").count() >= 3, + "#9248: expected accumulator plus two raw f64 loads:\n{fast_body}" ); + for instruction in ["fmul double", "fadd double", "store double"] { + assert!( + fast_body.contains(instruction), + "#9248: missing `{instruction}` in the packed fast body:\n{fast_body}" + ); + } + for fallback in [ + "@js_typed_feedback_array_index_get_fallback_boxed", + "@js_dynamic_string_or_number_add", + "shadow.root.barrier", + ] { + assert!( + !fast_body.contains(fallback), + "#9248: packed fast body regained `{fallback}`:\n{fast_body}" + ); + } for moving_gc in [false, true] { assert_stdout( &run(&bin, dir.path(), moving_gc),