Skip to content

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

Description

@proggeramlug

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.

function run(a: number[]): number {
  let c = 0.0;
  for (let r = 0; r < 2000; r++) {
    for (let k = 1; k < 4096; k++) {     // <-- `a.length` here is 7.75x faster
      if (a[k] > 0.0) c++;
    }
  }
  return c;
}

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:

  1. 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.
  2. 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.

https://claude.ai/code/session_01Pcq6j6y57TdKSR2Zx2D187

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions