You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
benchmarks/suite/16_matrix_multiply.ts is 3.1× Node on the Mac mini (100 ms vs 32) and 4.2× on the Linux box (96 vs 23). The interesting part is where the time is not.
Everything is inside the generated function. The typed-feedback guard calls visible in the IR are cold-arm bookkeeping; the steady state already takes the inline read tier (arr.guard.deref / .live / .range). So this is a code-quality problem, not a call-elimination one — worth stating because the IR call counts suggest otherwise at a glance.
What the inner loop actually executes
perf annotate on that symbol, hottest instructions:
2.32 cmp $0xf42400,%r9d ; 16,000,000 — the length_sane bound
2.08 cmp $0xf42400,%r9d ; …and capacity_sane
1.82 mov (%rax),%eax ; header length load
1.32 movzwl -0x6(%rax),%ecx ; _reserved flags word
1.31 movabs $0x7ffd000000000000 ; NaN-box tag mask
1.56 test %r8b,%r8b ; integrity / prototype flags
1.56 vcvttsd2si %xmm0,%r14 ; index double→i32
The vmulsd/vaddsd doing the actual work are nowhere near the top. Per iteration, for botha and b, the code re-derives: pointer tag check, handle-band check, header dereference, _reserved flag tests, the two 16M sanity comparisons, and the bounds check — for receivers that are loop-invariant parameters whose headers cannot change inside the loop.
The IR tells the same story: the specialised function has 27 shadow-root stores and 5 atomic PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT loads to accomplish 2 load doubles and the multiply-add.
LLVM cannot hoist any of it: the guard reloads the header through a pointer it cannot prove unaliased, and the atomic read is a motion barrier.
Why the existing hoisting machinery does not apply
The versioned packed clone exists to do exactly this — guard once at loop entry, raw loads inside — but matmul's inner loop is not admitted, for three independent reasons:
there are three receivers (a, b read; c stored), where the clone guards one.
Shape of the fix
Generalise the versioned clone so a counted loop over loop-invariant receivers can hoist their guards into the preheader and keep only a per-read icmp ult idx, len inside — the structure #9161 already emits for a foreign counter, extended to (1) a loop-invariant local/param bound and (2) arbitrary proven-i32 index expressions, over (3) a receiver set.
That is a real piece of work rather than a tweak, which is why this is an issue and not a PR. Filing it with the measurement so the next attempt starts from the instruction-level cause rather than from the IR's misleading call counts.
benchmarks/suite/16_matrix_multiply.tsis 3.1× Node on the Mac mini (100 ms vs 32) and 4.2× on the Linux box (96 vs 23). The interesting part is where the time is not.There are no runtime calls to remove
Everything is inside the generated function. The typed-feedback guard calls visible in the IR are cold-arm bookkeeping; the steady state already takes the inline read tier (
arr.guard.deref/.live/.range). So this is a code-quality problem, not a call-elimination one — worth stating because the IR call counts suggest otherwise at a glance.What the inner loop actually executes
perf annotateon that symbol, hottest instructions:The
vmulsd/vaddsddoing the actual work are nowhere near the top. Per iteration, for bothaandb, the code re-derives: pointer tag check, handle-band check, header dereference,_reservedflag tests, the two 16M sanity comparisons, and the bounds check — for receivers that are loop-invariant parameters whose headers cannot change inside the loop.The IR tells the same story: the specialised function has 27 shadow-root stores and 5 atomic
PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNTloads to accomplish 2load doubles and the multiply-add.LLVM cannot hoist any of it: the guard reloads the header through a pointer it cannot prove unaliased, and the atomic read is a motion barrier.
Why the existing hoisting machinery does not apply
The versioned packed clone exists to do exactly this — guard once at loop entry, raw loads inside — but
matmul's inner loop is not admitted, for three independent reasons:k < size), notarr.length, soclassify_for_length_hoist_impldeclines. (classify_for_local_boundexists but serves a different optimisation — making the loop condition an i32 compare, issue i32 loop counter not specialized when loop bound is a 'number' parameter — blocks LoopVectorizer on Buffer-read hot paths #168 — not clone admission.)a[i * size + k]), so the clone's counter-keyed read fact does not fit; these need a per-read bounds check, the mechanism perf(codegen): packed clones read a foreign counter inline (10_nested_loops 51 → 17 ms, Node parity) #9161 introduced for foreign counters.a,bread;cstored), where the clone guards one.Shape of the fix
Generalise the versioned clone so a counted loop over loop-invariant receivers can hoist their guards into the preheader and keep only a per-read
icmp ult idx, leninside — the structure #9161 already emits for a foreign counter, extended to (1) a loop-invariant local/param bound and (2) arbitrary proven-i32 index expressions, over (3) a receiver set.That is a real piece of work rather than a tweak, which is why this is an issue and not a PR. Filing it with the measurement so the next attempt starts from the instruction-level cause rather than from the IR's misleading call counts.
https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT