Skip to content

JIT: emit float CMOV as branch-around-mov instead of flag-preserving blend - #973

Open
trethaller wants to merge 1 commit into
HaxeFoundation:hl2_ir_rebasefrom
trethaller:float-mov-optim
Open

JIT: emit float CMOV as branch-around-mov instead of flag-preserving blend#973
trethaller wants to merge 1 commit into
HaxeFoundation:hl2_ir_rebasefrom
trethaller:float-mov-optim

Conversation

@trethaller

@trethaller trethaller commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude summary

The float conditional-move (used to resolve a float phi on a conditional edge, e.g. a cond ? a : b float ternary) had no cmovcc for XMM, so it was lowered to a branchless mask blend (setcc/neg/movq/andnpd/andpd/orpd). That arithmetic clobbers EFLAGS, but a following JCOND re-derives its condition from the same shared CMP (get_cond_jump skips over CMOV), so the blend had to be bracketed with pushfq/popfq to keep the flags alive. popfq is heavily microcoded and serializes the pipeline: on a hot particle-update loop it dominated the Core-Bound cycles (VTune: ~44% Serializing Operations).

Emit a short jcc that skips a plain mov instead. A jcc reads EFLAGS but never writes them, so the shared compare survives naturally for the trailing JCOND. This drops the pushfq/popfq, the whole blend, and the stack spill the memory-operand path needed for ANDPD alignment. It mirrors the existing float CXCHG lowering right below this case. ~6% faster on the particle benchmark; output verified identical against the previous codegen (see Test_cmov.hx).

I'll be honest I didn't get any of that

How I got here

I was benchmarking hide Emitter particles with VTune and noticed a lot of time was spend on a simple if(myenum != VZero) which should have been cheap.
The time was showing up under "Serializing Operations" which seemed weird, I'd have expected either cache miss on resolving the enum or branch prediction issues.
Feeding the assembly code to Claude led to this:

Haxe: if (def.dampen != hrt.prefab.fx.Value.VZero) { ... } in the per-particle loop (hrt.prefab.fx.ParticleInstance). def.dampen is loop-invariant — same for every particle, every frame until the effect is edited. The != VZero test, a null check, and a switch(enumIndex val) are all being re-evaluated per particle.

The single comparison result is consumed twice:
As a branchless float select — setz → movzx/neg → movq xmm, then vandnpd/vandpd/vorpd to blend two doubles (resolving the dampen ternary).
As a real branch — the terminal jz <Block 122> that skips the whole if block.

Because the blend arithmetic (and sub rsp,8 / add rsp,8 register spills) clobbers EFLAGS, the encoder brackets it with pushfq / popfq to keep ZF alive for the jz. There are TWO such pairs on this line. popfq is heavily microcoded and drains the pipeline → this is the 43.6% Serializing Operations. All operands are scalar movsd → explains the 12.3% vectorization.

Results

Noticeable improvements in my particles benchmark.
Tested the new hl.exe with the SpaceCraft and Farever codebases and didn't notice any issue.

…blend

The float conditional-move (used to resolve a float phi on a conditional
edge, e.g. a `cond ? a : b` float ternary) had no cmovcc for XMM, so it
was lowered to a branchless mask blend (setcc/neg/movq/andnpd/andpd/orpd).
That arithmetic clobbers EFLAGS, but a following JCOND re-derives its
condition from the same shared CMP (get_cond_jump skips over CMOV), so the
blend had to be bracketed with pushfq/popfq to keep the flags alive. popfq
is heavily microcoded and serializes the pipeline: on a hot particle-update
loop it dominated the Core-Bound cycles (VTune: ~44% Serializing Operations).

Emit a short jcc that skips a plain mov instead. A jcc reads EFLAGS but
never writes them, so the shared compare survives naturally for the trailing
JCOND. This drops the pushfq/popfq, the whole blend, and the stack spill the
memory-operand path needed for ANDPD alignment. It mirrors the existing float
CXCHG lowering right below this case. ~6% faster on the particle benchmark;
output verified identical against the previous codegen (see Test_cmov.hx).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant