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
{{ message }}
Repository navigation
packed-f64 range tier rejects any if (...) counter++ body — a literal-bounded conditional-count loop is 7.75x slower than the identical arr.length-bounded one (3.1x node vs 0.40x) #9275
Sibling of #9259, and the mirror image of it: there the arr.length spelling was the slow one, here it is the fast one. Same underlying shape — two tiers with different admission power, and which one you get depends on how the bound is written.
The range tier admits c += a[k] but rejects any if (...) c++ body. The versioned tier admits both. So a conditional-count loop gets a packed clone only when its bound is arr.length.
Measured
Self-timed, min of 5, --no-cache --no-auto-optimize, 4096-element array, 2000 outer iterations. Every timing paired with a packed_f64.* block count from the emitted IR; all five fixtures produce node-identical output.
bound
body
packed blocks
perry
node
k < 4096
c += a[k]
12
8 ms
9 ms
0.89x
k < 4096
if (a[k] > 0.0) c++
0
31 ms
10 ms
3.10x
k < 4096
if (a[k] > a[k-1]) c++
0
49 ms
10 ms
4.90x
k < a.length
if (a[k] > 0.0) c++
10
4 ms
10 ms
0.40x
k < a.length
if (a[k] > a[k-1]) c++
12
19 ms
10 ms
1.90x
Rows 2 and 4 are the finding: identical body, only the bound spelling differs, 31 ms against 4 ms — a 7.75x swing that is the difference between losing to node by 3.1x and beating it by 2.5x.
functionrun(a: number[]): number{letc=0.0;for(letr=0;r<2000;r++){for(letk=1;k<4096;k++){// <-- `a.length` here is 7.75x fasterif(a[k]>0.0)c++;}}returnc;}
Where it declines
The versioned tier's body walker (expr_is_packed_f64_loop_safe) handles Expr::Compare by recursing into both operands, and integer c++ accumulators admit independently of the float set (admit_integer_update_accumulators). The range tier's body walk is narrower — its read-only mode is documented at stmt/loops.rs:1596 as taking "any number of scalar ..." statements, and an If carrying the read is not among them.
Note the versioned matcher emits no rejection for the literal-bound rows, because it never gets a candidate at all: with no arr.length in the condition there is no length hoist to classify, so it exits before any packed_loop_reject. The range matcher then declines silently. The result is a loop that falls off both tiers with no diagnostic from either — the range-matcher rejection trace in #9258 would have named it, which is a good argument for that trace.
Why this is worth fixing rather than documenting
for (let i = 0; i < N; i++) if (pred(a[i])) count++ is an extremely common shape, and a constant bound is the more natural spelling when the array length is known — which is exactly when perry does worst. Row 4 shows the tier is worth 0.40x node on this shape when it does apply, so the gap is not a small one being missed.
Two directions, in preference order:
Widen the range tier's body walk to admit a conditional whose branches are scalar statements, matching what the versioned tier already accepts. That keeps each tier's guard unchanged and only relaxes an admission shape.
Let the versioned tier accept a constant bound, so it can claim these loops. Larger change, and it duplicates a proof the range tier already has.
Method note
The four earlier attempts at this family (#9259 included) each began with a fixture that never reached the tier and therefore proved nothing, so every row above is paired with a block count from PERRY_LLVM_KEEP_IR=1 (path reported on stderr) rather than a timing alone. Row 1 is the positive control: it shares the literal bound with rows 2 and 3 and does get a clone, so the zeros in those rows are attributable to the body shape and not to the bound.
Activity
codegen: admit `if (...) c++` bodies in the packed-f64 range tier (Pe…
codegen: admit `if (...) c++` bodies in the packed-f64 range tier (#9275