[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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user