← board

x86-64 feature level, and how (or whether) to dispatch

UNBLOCKED 2026-08-19 — the baseline is DECIDED: x86-64-v2. Full rationale in [[decide-x86-64-baseline-for-arch-level-dispatch]] (decided/).

  • Compile to v2. SSE4.2/popcnt assumed.
  • Above v2 = runtime dispatch, never a baseline bump. Raising a compiled baseline later breaks every user below it — a one-way door, deliberately kept shut.
  • v1 is a possible future TARGET, not the baseline. Do not read the decision as "v1 is impossible"; if wanted it becomes a reduced/alternate target.

Do not raise the baseline above v2 without a plan for the gate box: plexus is Ivy Bridge (AVX, no FMA) = v2, so v3 code would SIGILL on the machine that verifies every commit. "It works on my desktop" is not evidence here.

The baseline row in the table below is now answered; the dispatch design is the remaining work.

BLOCKED on [[decide-x86-64-baseline-for-arch-level-dispatch]] as of 2026-08-19. This ticket states the reason itself — "The baseline row is the user's call, not an engineering one" — so whoever claims it has to guess something the project cannot un-choose. The measured constraint moved into the decision ticket: plexus, the box that gates every push, is Ivy Bridge (AVX, no FMA) = x86-64-v2, so a v3 baseline would SIGILL on the gate itself.

The levels, and where the interesting instructions sit

level adds earliest CPU
v1 SSE2 — the guaranteed x86-64 baseline 2003
v2 SSE3, SSSE3, SSE4.1/4.2, POPCNT, CMPXCHG16B Nehalem ~2009 / AMD ~2011
v3 AVX, AVX2, BMI1/2, FMA, LZCNT, MOVBE Haswell 2013 / AMD Zen 2017
v4 AVX-512 family 2016+, still not universal

pxx emits v1 today. The two things that have wanted a bump want different levels, and conflating them is how this gets decided wrongly:

The constraint that makes this urgent: our own gate box

The user's observation, 2026-08-15: plexus (Track T's test server) is from around that era and may well be pre-Haswell. If pxx emitted FMA unconditionally, plexus would SIGILL on the generated code — and it would surface as an incomprehensible compiler bug in a tstate report, not as "your CPU is too old", because nothing in the pipeline would say so.

MEASURED on the box itself, 2026-08-15 (the user opened neo@plexus for it):

model name : Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz   (Ivy Bridge EP, 2013)
12 cores
sse4_2 YES    avx YES    avx2 NO    fma NO

So plexus is x86-64-v2, not v3. Ivy Bridge has AVX but FMA and AVX2 both arrived with Haswell, one generation later. The suspicion was right and the consequence is hard:

Note the middle tier this exposes: plexus has AVX without FMA, so 256-bit VEX float work is available where FMA is not. Anything gated on "AVX" as a proxy for "modern" would pass the check and then crash on the FMA.

The dev box these measurements came from is an i7-6700 Skylake, which is v3 — so a local -mfma experiment passes here and dies there. Exactly the trap, and the reason this needs to be a policy rather than a per-session judgement.

CPU features are still not recorded in tstate (plexus.json and the report frontmatter carry host, sha, tier, wall, compiler_sha256 and nothing about the machine), so the next person has to ssh in again, and borg's level is still unknown. That gap is [[feature-t-record-host-cpu-features-in-tstate]].

"Dispatch would defeat inlining" — the objection, and the answer

The user's point, and it is correct as stated: a CPU-feature check is a one-time-per-process thing, so you would hold function pointers to the supported implementations — but you cannot inline through a function pointer, and inlining is most of why FMA helps a Horner chain in the first place.

The resolution is to dispatch at FUNCTION granularity, not instruction granularity. Compile the whole hot function twice — Sin_v3 using FMA throughout, Sin_v1 not — and bind the pointer once at startup. Each clone is internally fully inlined; the FMA contraction happens inside the clone, where it pays. What you lose is inlining through the dispatch point, at the outermost call only.

That is exactly what the C world does: glibc uses IFUNC (the dynamic linker resolves the symbol once, via a resolver function that reads CPUID), and gcc exposes it as __attribute__((target_clones("fma","default"))). It is a solved shape, not a novel one.

...but it does NOT generalize, and that limit decides the ticket

The user's follow-up, 2026-08-15: "so you suggest to inline, but make compile the outer function twice. so, what if the outer function is really a big draconian piece of code?" — correct, and the paragraph above was written off a cherry-picked example.

The thing you clone is whatever function contains the arithmetic, so the question is whose function that is:

Which is exactly why gcc does not multiversion user code automatically: target_clones is an attribute the author writes on a function they chose. For the general case gcc offers -march=/-mfma — a global baseline, decided once, by the human.

The rule underneath is granularity of work per dispatch, not function size:

Runtime dispatch pays only when there is a chunk of work big enough to amortize one indirect call.

memcpy qualifies (one call, N bytes) — which is why glibc multiversions it. Sin qualifies. Scalar a*b+c does not: one FMA is ~4 cycles and an indirect call is ~5, so per-operation dispatch is slower than having no FMA at all. The middle move — factor the FMA-able part into a cloned helper — only works if that helper does bulk work (a whole array, a whole block), because it reintroduces a call at that boundary. That is an algorithm restructuring, not a compiler switch.

So the option set collapses: cloning covers the RTL hot list and nothing else. Arbitrary user code can only be served by a compile-time baseline. Option 4 is therefore narrower than it first reads — it is not an alternative to option 3, it is a supplement to it for a dozen named functions.

The cost model that follows:

(Note Abs() is currently a real call even after hand-inlining, per [[feature-opt-float-register-temporaries]] — fix that first regardless.)

Options

  1. Stay v1. Do nothing. Costs the ~20% FMA was worth on float kernels. Everything runs everywhere including plexus. This is the status quo and it is defensible.
  2. Bump to v2 for the packed-double work. Low risk, ~universal, unlocks addsubpd/movddup. Does not give FMA.
  3. Opt-in compile flag (-mfma, or --cpu=v3). Default off, so nothing breaks; whoever knows their target gets the codegen. Cheapest way to have FMA at all, and the natural fit for a compiler that already has --target.
  4. Runtime multiversioning of a short list of RTL hot functions, per the shape above. Most machinery — pxx would need CPUID emission, a startup resolver, and a cloning pass — and per the limit above it buys only those named functions. User code still needs option 3.

A sane ladder is 3 then 4, with 2 decided independently on the packed-double ticket's own merits. Note 4 does not replace 3: even with full multiversioning, a user's own hot loop gets FMA only by compiling for a baseline.

User framing, 2026-08-15 — this is all OPTIMIZATION, and it is not urgent

"this is sortof two issues. 1 is optimization. and 2 is 'older' hardware support... but i consider this all optimization. for now, we are cool. and, in open source style — compile for 'native' (and native only) for best performance, or compile 'compat' or 'auto-detect cpu capabilities'. right now, not our worry but good to know."

Two things to carry forward from that:

Ordering — do this AFTER the value model, not before

The measured decomposition of pxx Sin vs glibc ([[feature-opt-float-register-temporaries]], 2026-08-15):

x86-64 float value model (Double carried as bits in RAX) 7.2x
FMA ~1.2x

The 7.2x needs no ISA bump at all — it runs on every x86-64 chip back to 2003, plexus included. FMA is a ~20% follow-on that costs a compatibility decision. Doing the ISA work first would be paying the hard price for the small win.

aarch64 needs none of this

FMADD is baseline in ARMv8 — no dispatch, no feature level, no clones. So all of the above is x86-64-only complexity, which is itself an argument for option 3 over option 4.

Gate

Whatever lands: make test + self-host byte-identical, plus a run on plexus specifically (not just the dev box) before anything ISA-gated is turned on by default — the whole point of the ticket is that the gate box may be the oldest machine in the fleet.

Triage 2026-08-19 (Track D re-triage pass)

Blocked on judgement, not on engineering — the fourth triage outcome. The ticket says so itself: "The baseline row is the user's call, not an engineering one. Everything below is the material for making it." A ticket typed feature with an unanswered policy question at its root reads as buildable, and whoever claims it has to guess a baseline the project cannot un-choose later.

Recommendation: split the baseline question into a decide- ticket (the same route feature-p-defineglobal-* took on 2026-08-19), leaving this one as the implementation that follows the answer. Filing it needs the user, so it is queued rather than taken.

Nothing here is stale: pxx still emits v1, and the ticket's own gate-box measurement — plexus is an Ivy Bridge Xeon with AVX but no FMA, i.e. v2, so a v3 bump would SIGILL on the machine that gates every push — is the load-bearing fact and is unchanged.