Skip to content

Arithmetic workers survive un-inlined in hot loop bodies #281

Description

@Unisay

Problem

Some hot loops pay a Lua function call per arithmetic operation because the floated dictionary-application binding (add = Data.Semiring.add semiringInt and friends, which purs floats to module level) survives as an uncurried worker instead of dissolving into the primop. The specialized tuple_fold loop compiles to

go_S_sc1Tuple_S_f1, go_S_sc1Tuple_S_f2 =
  Bench_TupleFold_add_S_w(go_S_sc1Tuple_S_f1, go_S_sc1Tuple_S_f2),
  Bench_TupleFold_add_S_w(go_S_sc1Tuple_S_f2, 1)

where Bench_TupleFold_add_S_w = function(x, y) return x + y end. Under PUC Lua 5.1 this is the whole remaining gap on the tuple_fold benchmark: 0.169s with the worker calls against 0.057s for the ideal loop with inline +, so about 3x is left on the table. Pre-existing behavior, not introduced by #208: a main-branch build of the same module produces the same worker calls.

The behavior is inconsistent across modules, which is what makes it look like a missed fold rather than a policy decision:

  • Bench.CurriedStep (one use each of add/sub/eq): everything dissolves, the loop body is acc + i, i - 1.
  • Bench.TupleFold (two uses of add): add$w survives, both call sites in the loop pay a call.
  • Golden.SpecConstr.Test (four uses of add, four of sub): add dissolves everywhere into inline +, while sub$w survives at all four sites, in the same module.

Approach

Diagnose before fixing: trace the pipeline (runStepsTraced) on Bench.TupleFold and Golden.SpecConstr.Test to find where add's resolution diverges, and why sub behaves differently from add in the same module (different classes resolve through different foreigns: Data.Semiring.intAdd against Data.Ring.intSub). Candidate mechanisms: the use-once whole-binding inline is the only path that fires reliably; the call-site paste (inlineSaturatedCall) either does not reach these workers or its budget/complexity gate declines them; or the inline-env snapshot goes stale across specialize rounds. The fix is likely a narrow one: a worker whose body is a bare PrimBinOp over its parameters is the cheapest possible paste and should inline at every saturated site regardless of use count.

Prerequisites / Relations

None blocking. Composes with #208: call-pattern specialization removes the per-iteration box, this issue is about the residual call-per-op in the same loops. Same machinery family as #24 (uncurry worker/wrapper) and #180 (budgeted call-site inlining). The tuple_fold benchmark and its counter goldens land with PR #279.

Verification / Measurement

bench/run on tuple_fold under PUC Lua 5.1: the linked artifact closes toward the ideal 0.057s from the current 0.169s (the boxed baseline was 0.425s). The loop body in bench/_build/Bench.TupleFold.lua shows inline + instead of add$w calls, and Golden.SpecConstr.Test's sub$w sites fold the same way. Existing eval goldens stay byte-identical.

Open questions

  • Why does add dissolve in Golden.SpecConstr.Test (four uses) but not in Bench.TupleFold (two uses), while sub survives with four uses in the module where add dissolved? The use-count hypothesis fails in both directions, so the actual trigger is still unidentified.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inlinerenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions