[iterate-4C] JIT native FP-arith Phase 2: fused madd family (the 27.7% op)

Native fmadds/fmsubs/fnmadds/fnmsubs + double siblings via hardware FMA
(vfmadd213sd/vfmsub213sd — single rounding, matching the interpreter's
correctly-rounded f64::mul_add; negated forms flip the result sign with xorpd,
and NaN — which the interpreter preserves unnegated — is caught by the finite
guard → deopt, so unconditional negate is exact on the fast path). Gated on a
runtime cpuid FMA3 check (emit::host_has_fma, cached OnceLock); on a non-FMA
host these arms fall back to the interpreter's software FMA — byte-identical, so
goldens hold on any host (NEVER emit mulsd+addsd, which double-rounds).

fp_arith_matches extended to the madd family (guarded on host FMA3). All 6-config
GOLDEN n200m BYTE-IDENTICAL; 24 xenia-jit tests green.

MEASURED (2B-instr run through the video, --gpu-inline, best-of-3) — CLEAN 3-way:
  interp                    72.6s  (27.5 MIPS)
  region JIT (pre-FP)       27.3s  (73.2 MIPS)  = 2.66x over interp
  region JIT + native FP    23.3s  (85.8 MIPS)  = 3.12x over interp
So native FP's OWN contribution is ~1.17x (+17%) on top of the region JIT — it
removed 62% of all interpreter fallbacks (306.6M→117.3M, the FP-arith share) but
that translates to only ~17% wall-time here because the region JIT already made
non-FP code fast and the --gpu-inline drain + plumbing dominate the remainder.
This matches the conservative ~1.3-1.5x plan estimate (region JIT is the bigger
CPU lever on the video; FP is a solid, deterministic increment and a multiplier
for a future multi-core mode). Video still ~7x from real-time → multi-core next.
This commit is contained in:
MechaCat02
2026-07-05 15:14:24 +02:00
parent 0672f37c9f
commit eb7c6f98cd
2 changed files with 64 additions and 0 deletions

View File

@@ -749,6 +749,49 @@ pub fn try_emit_native(
state.retire();
Emit::Native
}
// ===== Phase 2: fused multiply-add family (the 27.7% op). vfmadd/vfmsub
// give the single rounding the interpreter's `mul_add` uses; gated on host
// FMA3 (else fall back — byte-identical software FMA). Rc=1 → fallback. =====
PpcOpcode::fmaddsx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Madd, true);
state.retire();
Emit::Native
}
PpcOpcode::fmsubsx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Msub, true);
state.retire();
Emit::Native
}
PpcOpcode::fnmaddsx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmadd, true);
state.retire();
Emit::Native
}
PpcOpcode::fnmsubsx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmsub, true);
state.retire();
Emit::Native
}
PpcOpcode::fmaddx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Madd, false);
state.retire();
Emit::Native
}
PpcOpcode::fmsubx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Msub, false);
state.retire();
Emit::Native
}
PpcOpcode::fnmaddx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmadd, false);
state.retire();
Emit::Native
}
PpcOpcode::fnmsubx if !instr.rc_bit() && host_has_fma() => {
emit_fp_arith(ops, off, helpers.interpret_one, instr, FpArith::Nmsub, false);
state.retire();
Emit::Native
}
// ===== Rotate/mask (sh/mb/me are compile-time constants -> the 32/64-bit
// mask folds to a constant, so these are just rol + and). =====
@@ -1239,6 +1282,17 @@ fn emit_fp_store(ops: &mut Asm, off: &Offsets, helper: i64, rs: usize) {
);
}
/// Whether the host CPU has FMA3. Cached once. The `madd` family emits
/// `vfmadd`/`vfmsub` (single rounding, matching the interpreter's correctly-
/// rounded `f64::mul_add`); without FMA3 those arms must fall back to the
/// interpreter (whose `mul_add` is software FMA — still byte-identical, never a
/// `mulsd`+`addsd` that would double-round). Determinism holds on any host.
pub fn host_has_fma() -> bool {
use std::sync::OnceLock;
static FMA: OnceLock<bool> = OnceLock::new();
*FMA.get_or_init(|| std::arch::is_x86_feature_detected!("fma"))
}
/// A native FP-arithmetic op — selects operand roles and the x64 instruction.
#[derive(Clone, Copy)]
pub enum FpArith {

View File

@@ -928,6 +928,16 @@ fn fp_arith_matches() {
check_fp(enc_a(63, rd, ra, rb, 0, 20, 0), g, f);
check_fp(enc_a(63, rd, ra, 0, rc, 25, 0), g, f);
check_fp(enc_a(63, rd, ra, rb, 0, 18, 0), g, f);
// Phase 2: fused multiply-add family (XO 29/28/31/30 = madd/msub/nmadd/
// nmsub), single + double — only native when the host has FMA3.
if std::arch::is_x86_feature_detected!("fma") {
for &op in &[59u32, 63] {
check_fp(enc_a(op, rd, ra, rb, rc, 29, 0), g, f); // fmadd(s)
check_fp(enc_a(op, rd, ra, rb, rc, 28, 0), g, f); // fmsub(s)
check_fp(enc_a(op, rd, ra, rb, rc, 31, 0), g, f); // fnmadd(s)
check_fp(enc_a(op, rd, ra, rb, rc, 30, 0), g, f); // fnmsub(s)
}
}
}
}