cfallin opened PR #14228 from cfallin:opportunistic-value-defs to bytecodealliance:main:
Add a new lowering primitive, an "opportunistic def", which allows a lowering that incidentally computes another value to register that value for possible later use. When the backward scan reaches the value's actual definition, if the use-count has not grown (no further uses appeared while scanning up), the definition can be skipped entirely and the value aliased to the opportunistically-computed regs.
Also add a
value_usedhelper that reports whether a value still has uses, allowing skipping of some part of a lowering when not needed.With these two features available in the lowering environment, this PR then adds new lowerings for brif-of-
uadd_overflow, and bareuadd_overflow, on x86-64 and aarch64:
When
uadd_overflow's overflow flag is used as a branch condition, the branch directly uses flags produced by theaddinstruction, skipping (slow and verbose) materialization of the bool overflow flag into a GPR.If the sum is only used past the branch, then this
addalso produces that value; so only oneaddis ever emitted. Thus aadd; SETcc; test; jnzsequence turns into (x64)add; jb/ (aarch64)adds; b.hs.If the sum is used below the
uadd_overflowbut above the branch, anotheraddis also emitted. That's fine: adds are cheap; cheaper certainly than materializing flags withSETccorCSET.But to optimize that case further...
...when
uadd_overflowis only used for its sum, and not its overflow flag, we now emit anaddwithout aSETcc.As a result of these lowerings, the case
v1, v2 = uadd_overflow ...; brif v2, ...lowers toadd; jb, and the casev1, v2 = uadd_overflow ...; (use v1); brif v2, ...lowers toadd; (use sum); add; jb. The only remaining case where we use a slowSETccis when the bool result is actually materialized and used as an integer value; or if the add and branch are pushed into separate blocks.This resolves the same issue as #13919, following discussion in that PR and the Cranelift weekly meeting. In particular, (i) we do not build an ad-hoc separate scan (this mechanism works as part of the main lowering/instruction-selection scan); and (ii) we are resilient to instructions placed between the
uadd_overflowand branch, which is likely to happen due to egraph demand-based elaboration, and which foils a simpler peephole-based approach.<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease review the Bytecode Alliance's AI tool usage policy at
https://github.com/bytecodealliance/governance/blob/main/AI_TOOL_POLICY.mdPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
cfallin requested alexcrichton for a review on PR #14228.
cfallin requested wasmtime-compiler-reviewers for a review on PR #14228.
cfallin commented on PR #14228:
(This has been on my TODO list for a long time; sorry about the delay, @playX18, but hopefully this addresses your use-case!)
:memo: alexcrichton submitted PR review:
Seems reasonable to me! Question on this: would it be possible to somehow determine, before fusing this into a branch, if the opportunistic def is going to be thrown away and re-calculated? For something like an addition it seems probably fine to always duplicate that, but for something like a multiplication it might be better to never duplicate that given its latency. (maybe? unsure). Basically it seems to me like a reasonable heuristic here would be to only fuse into branches where the opportunistic def actually works as the def, and in all other cases fall back to materializing the flag and then testing it later.
Although now that I actually write this down what I'm going for is to put the onus on CLIF producers to make sure the flag-and-branch are close together such that the opportunistic def always matches. In some sense that's no different from this PR as-is where it's still on them to do that to avoid the double-translate if it matters... Anyway, still curious on the question at least as a data point
:speech_balloon: alexcrichton created PR review comment:
Would it be possible to fold this into
is_nonzero_cmpinstead of having a rule specifically forbrif? That'd then additionally handle the condition flowing intp atrapz,trapnz, andselectall at the same time. If this does work, could this be done for x64 too?
:speech_balloon: alexcrichton created PR review comment:
Is this rule needed for the codegen tests that were added, or was this opportunistically added mostly to prove this out?
Additionally, could this skip using
addsand just useaddinstead? If so I think theConsumesFlagsNopinfrastructure can be removed.
:speech_balloon: alexcrichton created PR review comment:
Additionally, since we might want to use this for a number of other things, could a new helper like
produces_flags_with_opportunistic_defbe added? That'd internally then useproduces_flags_get_reg, callopportunistic_def, and then return aProducesFlagscreated withproduces_flags_ignore. Basically factoring out the boilerplate-y parts of this if possible
cfallin requested pchickey for a review on PR #14228.
cfallin requested wasmtime-core-reviewers for a review on PR #14228.
cfallin updated PR #14228.
:memo: cfallin submitted PR review.
:speech_balloon: cfallin created PR review comment:
Yes, this is what handles the "intermediate user" case (and removes the unused
SETcc). But yeah great point, I dunno why I kept that bit (copy+paste I guess); this is really just a plain old add. Updated!
cfallin commented on PR #14228:
Seems reasonable to me! Question on this: would it be possible to somehow determine, before fusing this into a branch, if the opportunistic def is going to be thrown away and re-calculated? For something like an addition it seems probably fine to always duplicate that, but for something like a multiplication it might be better to never duplicate that given its latency. (maybe? unsure). Basically it seems to me like a reasonable heuristic here would be to only fuse into branches where the opportunistic def actually works as the def, and in all other cases fall back to materializing the flag and then testing it later.
Although now that I actually write this down what I'm going for is to put the onus on CLIF producers to make sure the flag-and-branch are close together such that the opportunistic def always matches. In some sense that's no different from this PR as-is where it's still on them to do that to avoid the double-translate if it matters... Anyway, still curious on the question at least as a data point
Unfortunately we can't really know "the future" during the backward lowering scan (or, well, we could, but that would be a separate scan). The conclusion I came to is that it's way simpler to build this opportunistic mechanism and just let the lowering fire twice if there is actually a user in the block in between the add and branch.
I also address this with "If the sum is used below the uadd_overflow but above the branch, another add is also emitted. That's fine: adds are cheap; cheaper certainly than materializing flags with SETcc or CSET.", I think: what we get here is actually still cheaper than what you're asking for (strictly one fewer instruction --
add; add; branchrather thanadd; setcc; test; branch; and thesetccis slow).
:memo: cfallin submitted PR review.
:speech_balloon: cfallin created PR review comment:
I played with this a bit, but IMHO it's not really that much simpler to fold this into
ProducesFlags. It requires pulling apart all the pieces from the original inst anyway and stuffing them into theProducesFlags(the other result value, the emitted result from theadds) and requires thinking about the emission combinations with variousConsumesFlags. Arguably more brittle, and distributes the logic over a larger area. In other words I don't think it actually avoids any boilerplate, it just explodes it into shards of boiler iron embedded in the walls...
:memo: cfallin submitted PR review.
:speech_balloon: cfallin created PR review comment:
(Happy to tackle this or see someone else tackle this in a followup of course if there are more ideas here -- but maybe good to get the building blocks in first regardless?)
github-actions[bot] added the label cranelift on PR #14228.
github-actions[bot] added the label cranelift:area:machinst on PR #14228.
github-actions[bot] added the label cranelift:area:aarch64 on PR #14228.
github-actions[bot] added the label cranelift:area:x64 on PR #14228.
alexcrichton commented on PR #14228:
but for something like a multiplication it might be better to never duplicate that given its latency
Do you have thoughts on this?
Also something else I was thinking of recently, if an operation is loop-invariant we'd previously calculate it once outside of a loop and then reuse the flags/result within the loop, but now this'll force the arguments to the opertion to stay live for the entire loop. That in theory could add to register pressure which could have knock-on effects beyond the cost of the operation itself.
Maybe a heuristic that could be added is that the brif folding only happens when the flag brif is consuming was defined in the same basic block as the brif itself? That doesn't fully eliminate the double-translation but might help ward off pathological loop-like cases and I believe should also be pretty easy to implement.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Sorry but to clarify, which comment are you responding to of mine? The
is_nonzero_cmppart or theproduces_flags_with_opportunistic_defpart?For the helper part, to clarify I'm not asking for what I believe is anything too invovled, brittle, or fancy here. I'm envisioning:
diff --git a/cranelift/codegen/src/isa/aarch64/lower.isle b/cranelift/codegen/src/isa/aarch64/lower.isle index 10a1f01c31..aca4ee5e12 100644 --- a/cranelift/codegen/src/isa/aarch64/lower.isle +++ b/cranelift/codegen/src/isa/aarch64/lower.isle @@ -3170,10 +3170,14 @@ (second_result uadd_inst @ (uadd_overflow (ty_32_or_64 ty) x y)) _ _) (two_targets taken not_taken)) (if-let (first_result v1) uadd_inst) - (let ((producer ProducesFlags (alu_rrr_with_flags_paired ty x y (ALUOp.AddS))) - (sum Reg (produces_flags_get_reg producer)) - (_ Unit (opportunistic_def v1 sum))) - (emit_side_effect (br_cond_result (CondResult.Cond (produces_flags_ignore producer) (Cond.Hs)) taken not_taken)))) + (let ((producer ProducesFlags (alu_rrr_with_flags_paired ty x y (ALUOp.AddS)))) + (emit_side_effect (br_cond_result (CondResult.Cond (produces_flags_with_opportunistic_def v1 producer) (Cond.Hs)) taken not_taken)))) + +(decl produces_flags_with_opportunistic_def (Value ProducesFlags) ProducesFlags) +(rule (produces_flags_with_opportunistic_def val producer) + (let ((def Reg (produces_flags_get_reg producer)) + (_ Unit (opportunistic_def val def))) + (produces_flags_ignore producer))) ;; Helper to emit a branching instruction based on a `CondResult` (decl br_cond_result (CondResult MachLabel MachLabel) SideEffectNoResult)Personally I think a helper like that when other instances of this are added for subtract-with-overflow and multiply-with-overflow as it's less to juggle and duplicate across rules.
:memo: cfallin submitted PR review.
:speech_balloon: cfallin created PR review comment:
Ah, OK, I was trying to push the side-effects all to the
with_flagsemitter; so the opportunistic def itself would occur only when used. Cleaner in some sense but also exploded in complexity. I'll try to push this direction instead, thanks!
cfallin commented on PR #14228:
but for something like a multiplication it might be better to never duplicate that given its latency
Do you have thoughts on this?
Yep, agreed; this PR doesn't do the thing for multiply-with-overflow and IMHO it's probably good to keep it that way.
Also something else I was thinking of recently, if an operation is loop-invariant we'd previously calculate it once outside of a loop and then reuse the flags/result within the loop, but now this'll force the arguments to the opertion to stay live for the entire loop. That in theory could add to register pressure which could have knock-on effects beyond the cost of the operation itself.
Maybe a heuristic that could be added is that the brif folding only happens when the flag brif is consuming was defined in the same basic block as the brif itself? That doesn't fully eliminate the double-translation but might help ward off pathological loop-like cases and I believe should also be pretty easy to implement.
Yeah, this is an interesting tradeoff space, and unfortunately is NP-hard. Basically what you're asking is can we combine instruction selection and regalloc to be aware of each other with their cost heuristics, i.e. don't fuse or choose different instructions if it would result in register pressure; there are PhD theses on this (see e.g. David Koes from CMU).
What I would say is that example-driven thinking I think will not necessarily lead us to a globally optimal place here; for any heuristic we come up with, we can come up with a counterexample. (For example: the same-block-only heuristic is brittle in the face of code motion or CFG edits that might otherwise be totally reasonable: GVN hoisting a common add that occurs in two sub-branches; or, a CLIF-level edge splitting that a producer might do; or ...)
I think that if we want to relitigate instruction fusion in general, we should have the discussion at a high level and evaluate it objectively. Experience has shown so far that a "maximal" instruction fusion approach generally results in better code and perf. Someone else is welcome to run experiments changing these heuristics globally and see what it does on Sightglass; naively I'd expect some negative movement but we'd have to see.
cfallin edited a comment on PR #14228:
but for something like a multiplication it might be better to never duplicate that given its latency
Do you have thoughts on this?
Yes -- this PR doesn't do the thing for multiply-with-overflow and IMHO it's probably good to keep it that way.
Also something else I was thinking of recently, if an operation is loop-invariant we'd previously calculate it once outside of a loop and then reuse the flags/result within the loop, but now this'll force the arguments to the opertion to stay live for the entire loop. That in theory could add to register pressure which could have knock-on effects beyond the cost of the operation itself.
Maybe a heuristic that could be added is that the brif folding only happens when the flag brif is consuming was defined in the same basic block as the brif itself? That doesn't fully eliminate the double-translation but might help ward off pathological loop-like cases and I believe should also be pretty easy to implement.
Yeah, this is an interesting tradeoff space, and unfortunately is NP-hard. Basically what you're asking is can we combine instruction selection and regalloc to be aware of each other with their cost heuristics, i.e. don't fuse or choose different instructions if it would result in register pressure; there are PhD theses on this (see e.g. David Koes from CMU).
What I would say is that example-driven thinking I think will not necessarily lead us to a globally optimal place here; for any heuristic we come up with, we can come up with a counterexample. (For example: the same-block-only heuristic is brittle in the face of code motion or CFG edits that might otherwise be totally reasonable: GVN hoisting a common add that occurs in two sub-branches; or, a CLIF-level edge splitting that a producer might do; or ...)
I think that if we want to relitigate instruction fusion in general, we should have the discussion at a high level and evaluate it objectively. Experience has shown so far that a "maximal" instruction fusion approach generally results in better code and perf. Someone else is welcome to run experiments changing these heuristics globally and see what it does on Sightglass; naively I'd expect some negative movement but we'd have to see.
alexcrichton commented on PR #14228:
Sorry I think I'm being misinterpreted, I'm not asking for intertwining regalloc and instruction selection as I'm familiar with how you've said a number of times historically that it's basically not practical. I'm additionally not asking to relitigate anything here.
What I'm trying to poke at is that this change isn't being made in isolation, but I view this as a solution to the problem of fusing oveflow-producing things with conditional branches. This is one of a possible number of ways to solve this, and in reviewing this I'm pointing out how I don't believe that this is a universal solution as-is. For example I understand you're not proposing adding multiplication here, and nor am I proposing that multiplication be added here. Despite that though I want to point out that I'm not sure it would make sense to use this infrastructure to add multiplication handling because of the risk of duplicating instructions and the latency of multiplication.
In terms of evaluation, the original PR https://github.com/bytecodealliance/wasmtime/pull/13919 here points to this zulip conversation which was specifically about multiplication overflow for example. In the spirit of objective evaluation, should this perhaps be benchmarked in the context of the original project to see how much this helps things? Not by you @cfallin but @playX18 would you be able to help out benchmarking this change? I'd be surprised if Sightglass were set up to take advantage of these new lowering rules since wasm doesn't really use
*_overflowinstructions.In the absence of benchmarks I agree I don't know how best to evaluate this -- is duplication of the
addalways the right option, or are there edge cases that would push the heursitics in a different direction? Is the same-block-definition rule too brittle to miss out on enough optimizations, or does it cover most of the intended use cases? I'm not sure myself...
cfallin commented on PR #14228:
In the absence of benchmarks I agree I don't know how best to evaluate this -- is duplication of the add always the right option, or are there edge cases that would push the heursitics in a different direction? Is the same-block-definition rule too brittle to miss out on enough optimizations, or does it cover most of the intended use cases? I'm not sure myself...
Right, and I'm claiming that this is itself relitigating our consensus on instruction fusion (whether or not you mean to). It's true in far more cases than just here that fusing producer instructions into consumers can extend liveranges.[^1] We've accepted this, and we've generally seen better code as a result.
The unique bit here is that we potentially codegen
addtwice (as we do for other kinds of flag producers, too, BTW -- every use of anicmpthat fuses will codegen acmp, which uses the same ALU asadd). But that's already addressed by the argument above that the dynamic instruction path length is actually shorter with this PR:add; add; jofor the case with use-in-the-middle is strictly better thanadd; seto; test; jnz.I don't believe that this is a universal solution as-is.
Nothing is, I believe; that's the frustrating bit about compilers! :-)
[^1]: For a simple example: on aarch64, we can fuse shifts and extends into addressing modes. If someone else also uses the result of the shift/extend directly, we'll also codegen that op separately. That's fine; ALU ops are cheap; doing better is (again) an NP-hard weighted-set-cover problem. The idea of "maximal munch" (Muchnick book's term) is that it's a pretty good heuristic on average.
alexcrichton commented on PR #14228:
Ok I see what you mean yeah, and I think it's fine to set aside the double-translation issue. I'd still like to see if it's possible to integrate with
is_nonzero_cmp, though, and poking through the genesis of the feature request shows that this may not actually solve the original request as it was via multiplication, but that doesn't mean that this can't land separately. (although the point about maybe-lowering-two-mul-is-net-worse I feel is still open)
cfallin commented on PR #14228:
this may not actually solve the original request as it was via multiplication
FWIW, thinking about it some more, we could include
*mul_overflowtoo, with the following reasoning: it's a separate opcode, so it's allowed to have a separate cost profile. And "will be exactly as cheap as a normal multiply as long as you don't also use it between the op and the branch-on-overflow" seems like a reasonable rule. Keeping in mind that the not-fused case has a cost on the materialized overflow path as well, the matrix is:
- no fusion, no in-the-middle use of product output: 4 insts, 6 cycles (
mul; seto; test; jnz)- no fusion, in-the-middle use of product output: 4 insts, 6 cycles min, partial register stall(*) (
mul; seto; ...; test; jnz)- fusion, no in-the-middle use of product output: 2 insts, 4 cycles (
mul; jo)- fusion, in-the-middle use of product output: 3 insts, 7 cycles (
mul; ...; mul; jo)(*) partial register stall:
seto %al(or any 8-bit reg) has an input dependency on whatever was previously in that register, since the upper 56 bits are unchanged; so it potentially waits longer in the OoO scheduling for that previous producer to finish, even though it's unused by thetest %al, %allater.So it seems almost always better to me to do the fusion; (i) the producer can know the cost profile, (ii) it's a big win in the good case, (iii) it's 1 cycle penalty in the bad case, still avoids the unknown partial-register stall penalty, and it's still statically shorter code.
alexcrichton commented on PR #14228:
Oh I sort of naively expected multiplication to be 50-100 cycles, if it's just 3 then it seems fine to me to lump it in the same bucket as adds
cfallin updated PR #14228.
:memo: cfallin submitted PR review.
:speech_balloon: cfallin created PR review comment:
OK, pushed it all to the produces/consumes-flags mechanism, including the side-effect itself (emitted by
with_flags), which turns out to be needed to make the nice folding work with a flags consumer that also consumes the other value (e.g.,uadd_overflow+select-- hypothetically useful for a saturating add?), because of the ordering of the opportunistic def + processing of args in the outermost consumer.
cfallin commented on PR #14228:
Updated all, but I'll push a
umul/smulcase too.Also the deeper surgery to
ProducesFlagsfrom review feedback meant I had to go (one-level-)recursive, and now I need to fix verification -- TBD, hopefully not too bad.
cfallin updated PR #14228.
:thumbs_up: alexcrichton submitted PR review:
Oh I'd be fine with what I sketched to avoid more
ProducesFlagsvariants, but if you feel this is better that seems ok too
cfallin commented on PR #14228:
Yeah, I went further with putting the effect inside
with_flagsonce I saw theuadd_overflow+selectcase have an extraadd; we really should be able to do that (and now we do). It's a one-time cost and the proper framework solution so I'm fine with it now that it's done :-)
cfallin commented on PR #14228:
OK, actually I'll do muls in a separate PR -- they're getting kind of nontrivial (new variants since the sequences are not just a
mul+ branch). I'll go ahead and merge this now on your r+ -- thanks!
cfallin has enabled auto merge for PR #14228.
cfallin added PR #14228 Cranelift: add opportunistic value defs, use for uadd_overflow + brif folds. to the merge queue
:check: cfallin merged PR #14228.
cfallin removed PR #14228 Cranelift: add opportunistic value defs, use for uadd_overflow + brif folds. from the merge queue
Last updated: Sep 20 2026 at 20:07 UTC