Skip to content

x86_64: branchless fxmin/fxmax via cmp + cmovcc - #1093

Open
ufo5260987423 wants to merge 1 commit into
cisco:mainfrom
ufo5260987423:pr-cmov-fxminmax
Open

ufo5260987423 wants to merge 1 commit into
cisco:mainfrom
ufo5260987423:pr-cmov-fxminmax

Conversation

@ufo5260987423

Copy link
Copy Markdown
Contributor

This PR compiles 2-argument fxmin/fxmax at optimize level 3 on x86_64 to a compare and a conditional move instead of a compare and branch, eliminating branch-misprediction stalls when the comparison direction is data-dependent. Multi-argument calls inherit the same sequence through the existing reduce path.

New %ipmin/%ipmax value instructions select a cmp/cmovcc sequence by destination/source aliasing:

  • dest == src1: cmp dest, src2 (flags = src2 − src1), then cmovl/cmovg from src2
  • dest == src2: cmp src1, dest (flags = dest − src1), then cmovg/cmovl from src1 with the comparison direction swapped
  • otherwise: preset dest = src1 first (aliasing nothing, so the move cannot clobber a source) and take the first case

No scratch register is needed in any case. Unlike the flonum flmin/flmax story in #1084, ties are unobservable here — fixnums are immediates, so either operand register gives the same result.

cmovcc is part of the original x86_64 baseline, so unlike the SSE4.1 roundsd there is no hardware-level portability question; the new ipmin-instruction machine constant (enabled in a6.def, defaulting to #f in default.def, like popcount-instruction) keeps other architectures on the existing compare-and-branch inline.

One encoding subtlety worth calling out for review: cmov-op swaps the ax-ea-modrm-reg arguments relative to binary-op — unlike mov/add (dest = r/m), CMOVcc encodes the destination in the modrm reg field and the source as r/m, and the REX R/B bits follow the same swap. The comment in cmov-op documents this, since getting it wrong silently clobbers the source.

Performance (i7-8550U, optimize level 3, 2M operations per measurement, fixed seed with identical checksums both sides, gate flipped via ipmin-instruction with boot md5 verified per side):

workload branch pattern before after
rand-rand 50/50 unpredictable 14.15 ms 3.6–3.9 ms
rand-randmax 50/50 unpredictable 11.10 ms 3.6–3.8 ms
rand-const almost always not-taken 4.16 ms 3.9–4.1 ms
const-rand almost always taken 3.72 ms 3.8–4.0 ms

About 3x on unpredictable data; neutral on predictable data. The compiler itself uses fxmin/fxmax for frame-size bookkeeping (small values, highly predictable), so per the const-rand row compile-time impact is approximately zero.

No new mats: fxmin/fxmax are already covered by mats/fx.ms (boundary values, arity errors) and mats/cptypes.ms (inlining behavior), and the transformation preserves results exactly; both pass at all optimize levels. Happy to add targeted tests if preferred.

A release_notes entry is included.

Comment thread s/cpprim.ss Outdated
(constant-case ipmin-instruction
[(#t)
; branchless cmp + cmovcc (see asm-ipminmax in x86_64.ss)
(bind #t (e1 e2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the #t here can be #f, since e1 and e2 are each referenced only once.

Comment thread s/cpprim.ss Outdated
[(e1 e2)
(constant-case ipmin-instruction
[(#t)
(bind #t (e1 e2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Add %ipmin/%ipmax value instructions selecting cmp/cmovcc sequences by
dest/source aliasing; no scratch register is needed in any case.  Gate
behind ipmin-instruction (a6.def #t, default #f) and route the o=3
fxmin/fxmax inlines through it.

cmov-op swaps the ax-ea-modrm-reg arguments relative to binary-op:
unlike mov/add (dest = r/m), CMOVcc encodes the destination in the
modrm reg field.  Getting this wrong silently clobbers the source, and
because the compiler's own frame-size bookkeeping uses fxmin/fxmax,
the resulting heap corruption surfaces as inscrutable GC failures far
from the cause.
@ufo5260987423

Copy link
Copy Markdown
Contributor Author

Thanks @mflatt - both suggestions applied: the cmov paths now use (bind #f (e1 e2) ...) since each operand is referenced only once. The branch is also rebased onto current main to resolve the conflict with the recently merged foreign-call repairs. Rebuilt and re-verified after the rebase: boot md5 consistent, fx.ms and cptypes.ms pass at optimize level 3, and the codegen dump still shows the expected cmp + cmovcc sequences.

This branch has not been deployed

No deployments
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.

2 participants