From ee6fd7be8f8323040c12d83bd7a17ccac8877f95 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 30 Jul 2026 08:59:21 +0800 Subject: [PATCH 1/2] [ARM64_DYNAREC] Reduce TEST/CMP safe-flag emission overhead Route TEST Eb,imm8 (0xF6 /0) through emit_test8c instead of materializing the immediate into a scratch, store {x,w}ZR into the deferred op2 of emit_cmp{32,16,8}_0 instead of a zeroed scratch, and add ZF/PEND-only fast paths to emit_test{16,16c,8c} that use unshifted ANDS/ANDSw_mask rather than shifting the byte or word result to bit 31 and back. Measured on Ampere eMag 8180, default safe flags, static benchfloat-O2 native instruction count: 85,587 to 85,070, a drop of 517 (about 68% of the 755 instruction gap between the safe default and SAFEFLAGS=0). Runtime is unchanged (n=15 median 637.8k vs 637.7k KFLOPS): the removed ops are cheap MOV and shift ALU that the out-of-order core hides behind memory latency. This is a codegen cleanup, not a runtime win. A self-checking flag test passes under SAFEFLAGS 0/1/2 and against the DYNAREC=0 interpreter; a dump confirms the fast-path shape. Operands are zero-extended by GETEW/GETGW/GETEB, so the unshifted ANDS yields correct ZF, and the UpdateFlags recompute dynablock forces gen_flags=X_ALL, so the ZF/PEND fast path cannot fire during signal reconstruction. The byte-register emit_test8 form is deliberately left on the full path, since adding the fast path there regressed the count. --- src/dynarec/arm64/dynarec_arm64_00.c | 3 +- src/dynarec/arm64/dynarec_arm64_emit_tests.c | 54 +++++++++++++++++--- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/dynarec/arm64/dynarec_arm64_00.c b/src/dynarec/arm64/dynarec_arm64_00.c index ff29f0640c..958b10a15a 100644 --- a/src/dynarec/arm64/dynarec_arm64_00.c +++ b/src/dynarec/arm64/dynarec_arm64_00.c @@ -4068,8 +4068,7 @@ uintptr_t dynarec64_00(dynarec_arm_t* dyn, uintptr_t addr, uintptr_t ip, int nin SETFLAGS(X_ALL, SF_SET_PENDING); GETEB(x1, 1); u8 = F8; - MOV32w(x2, u8); - emit_test8(dyn, ninst, x1, x2, x3, x4, x5); + emit_test8c(dyn, ninst, x1, u8, x2, x4, x5); break; case 2: INST_NAME("NOT Eb"); diff --git a/src/dynarec/arm64/dynarec_arm64_emit_tests.c b/src/dynarec/arm64/dynarec_arm64_emit_tests.c index 73c467e42b..60cda00ff2 100644 --- a/src/dynarec/arm64/dynarec_arm64_emit_tests.c +++ b/src/dynarec/arm64/dynarec_arm64_emit_tests.c @@ -80,9 +80,8 @@ void emit_cmp32(dynarec_arm_t* dyn, int ninst, rex_t rex, int s1, int s2, int s3 void emit_cmp32_0(dynarec_arm_t* dyn, int ninst, rex_t rex, int s1, int s3, int s4) { IFX_PENDOR0 { - MOV64xw(s4, 0); STRxw_U12(s1, xEmu, offsetof(x64emu_t, op1)); - STRxw_U12(s4, xEmu, offsetof(x64emu_t, op2)); + STRxw_U12(xZR, xEmu, offsetof(x64emu_t, op2)); STRxw_U12(s1, xEmu, offsetof(x64emu_t, res)); SET_DF(s4, rex.w?d_cmp64:d_cmp32); } else { @@ -198,9 +197,8 @@ void emit_cmp16(dynarec_arm_t* dyn, int ninst, int s1, int s2, int s3, int s4, i void emit_cmp16_0(dynarec_arm_t* dyn, int ninst, int s1, int s3, int s4) { IFX_PENDOR0 { - MOV32w(s3, 0); STRH_U12(s1, xEmu, offsetof(x64emu_t, op1)); - STRH_U12(s3, xEmu, offsetof(x64emu_t, op2)); + STRH_U12(wZR, xEmu, offsetof(x64emu_t, op2)); STRH_U12(s1, xEmu, offsetof(x64emu_t, res)); SET_DF(s3, d_cmp16); } else { @@ -291,8 +289,7 @@ void emit_cmp8_0(dynarec_arm_t* dyn, int ninst, int s1, int s3, int s4) { IFX_PENDOR0 { STRB_U12(s1, xEmu, offsetof(x64emu_t, op1)); - MOV32w(s4, 0); - STRB_U12(s4, xEmu, offsetof(x64emu_t, op2)); + STRB_U12(wZR, xEmu, offsetof(x64emu_t, op2)); STRB_U12(s1, xEmu, offsetof(x64emu_t, res)); SET_DF(s3, d_cmp8); } else { @@ -410,6 +407,19 @@ void emit_test16(dynarec_arm_t* dyn, int ninst, int s1, int s2, int s3, int s4, } else { SET_DFNONE(); } + if((dyn->insts[ninst].x64.gen_flags&(X_ZF|X_PEND)) && !(dyn->insts[ninst].x64.gen_flags&~(X_ZF|X_PEND))) { + ANDSw_REG(s5, s1, s2); + IFX(X_PEND) { + STRH_U12(s5, xEmu, offsetof(x64emu_t, res)); + } + IFX(X_ZF) { + IFNATIVE(NF_EQ) {} else { + CSETw(s4, cEQ); + BFIw(xFlags, s4, F_ZF, 1); + } + } + return; + } IFX(X_CF|X_ZF|X_SF|X_OF) { LSLw(s5, s1, 16); ANDSw_REG_LSL(s5, s5, s2, 16); @@ -458,6 +468,22 @@ void emit_test16c(dynarec_arm_t* dyn, int ninst, int s1, uint32_t c, int s3, int } else { SET_DFNONE(); } + if((dyn->insts[ninst].x64.gen_flags&(X_ZF|X_PEND)) && !(dyn->insts[ninst].x64.gen_flags&~(X_ZF|X_PEND))) { + mask = convert_bitmask_w(c); + if(mask) { + ANDSw_mask(s5, s1, mask&0x3F, (mask>>6)&0x3F); + IFX(X_PEND) { + STRH_U12(s5, xEmu, offsetof(x64emu_t, res)); + } + IFX(X_ZF) { + IFNATIVE(NF_EQ) {} else { + CSETw(s4, cEQ); + BFIw(xFlags, s4, F_ZF, 1); + } + } + return; + } + } IFX(X_CF|X_ZF|X_SF|X_OF) { LSLw(s5, s1, 16); mask = convert_bitmask_w(c<<16); @@ -565,6 +591,22 @@ void emit_test8c(dynarec_arm_t* dyn, int ninst, int s1, uint32_t c, int s3, int } else { SET_DFNONE(); } + if((dyn->insts[ninst].x64.gen_flags&(X_ZF|X_PEND)) && !(dyn->insts[ninst].x64.gen_flags&~(X_ZF|X_PEND))) { + mask = convert_bitmask_w(c); + if(mask) { + ANDSw_mask(s5, s1, mask&0x3F, (mask>>6)&0x3F); + IFX(X_PEND) { + STRB_U12(s5, xEmu, offsetof(x64emu_t, res)); + } + IFX(X_ZF) { + IFNATIVE(NF_EQ) {} else { + CSETw(s4, cEQ); + BFIw(xFlags, s4, F_ZF, 1); + } + } + return; + } + } IFX(X_CF|X_ZF|X_SF|X_OF) { LSLw(s5, s1, 24); mask = convert_bitmask_w(c<<24); From 5b2b158b9d6f14f96aae928d03f15a14163ae7cc Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 30 Jul 2026 08:59:21 +0800 Subject: [PATCH 2/2] [ARM64_DYNAREC] Carry native flags across block boundary A flag producer that ends a block has X_PEND forced onto it, so the deferred op1/op2/res spill keeps full EFLAGS reconstructable for any successor or signal. When the producer's whole real flag need (gen_flags & X_ALL, restricted to ZF/SF/CF/OF) reaches its downstream consumers purely through native ARM NZCV, as in the common loop-control CMP feeding a native Jcc, that spill is redundant for the branch. updateNativeFlags gains a guarded pass, reusing getNativeFlagsUsed for the coverage proof, that clears X_PEND on such producers so emit_{cmp, test} take the SET_DFNONE path. ZF/SF/CF/OF stay reconstructable at faults through adjust_arch's NZCV overlay (kept in need_nat_flags); only PF/AF turn approximate across the block exit, which the dynarec already is for native flags and which the x86 ABI (arithmetic flags not preserved across calls) makes safe. Controlled by BOX64_DYNAREC_LOOPFLAGS. Effect: the daxpy_ur loop-control CMP drops from 6 emitted ops (STR op1/op2/df/res + SUBS) to 1 (SUBS); the JNZ still branches on native NZCV. Measured on Ampere eMag 8180, benchfloat-O2, default safe flags, n=15 alternating core-pinned, BOX64_DYNACACHE=0: [off] median 637,780 KFLOPS (MAD 230, range 636,542..638,108) [on] median 766,577 KFLOPS (MAD 389, range 761,139..767,342) +20.2% The two distributions are fully disjoint. The optimized figure matches SAFEFLAGS=0 (about 763k), so it recovers the entire default safe flag gap on this workload. A mid-block native CMP+Jcc already emits no deferred spill (gen_flags=X_ZF, no X_PEND); this brings the same treatment to the block-boundary case. --- src/dynarec/arm64/dynarec_arm64_functions.c | 28 +++++++++++++++++++++ src/include/env.h | 1 + 2 files changed, 29 insertions(+) diff --git a/src/dynarec/arm64/dynarec_arm64_functions.c b/src/dynarec/arm64/dynarec_arm64_functions.c index 91d6c7229a..2439a2d495 100644 --- a/src/dynarec/arm64/dynarec_arm64_functions.c +++ b/src/dynarec/arm64/dynarec_arm64_functions.c @@ -1167,6 +1167,34 @@ void updateNativeFlags(dynarec_native_t* dyn) if(flag2native(dyn->insts[ninst].x64.gen_flags,(dyn->insts[ninst].set_nat_flags&NF_PF_V)?1:0) && (dyn->insts[ninst].nat_flags_op==NAT_FLAG_OP_TOUCH)) { propagateNativeFlags(dyn, ninst); } + // BOX64_DYNAREC_LOOPFLAGS: a flag producer that ends a block has X_PEND forced + // onto it so the deferred op1/op2/res spill keeps full EFLAGS reconstructable for + // any successor or signal. When the producer's whole real flag need + // (gen_flags & X_ALL) is ZF/SF/CF/OF and reaches its downstream consumers purely + // through native NZCV (e.g. a loop-control CMP feeding a native Jcc), that spill is + // redundant for the branch. Clearing X_PEND makes emit_cmp/emit_test take the + // SET_DFNONE path. ZF/SF/CF/OF stay reconstructable at faults through adjust_arch's + // NZCV overlay (kept in need_nat_flags); only PF/AF turn approximate across the + // block exit, which the dynarec already is for native flags and which the x86 ABI + // (arithmetic flags not preserved across calls) makes safe across the exit. + if(BOX64ENV(dynarec_loopflags)) + for(int ninst=0; ninstsize; ++ninst) { + uint8_t gf = dyn->insts[ninst].x64.gen_flags; + if(!(gf&X_PEND) || dyn->insts[ninst].nat_flags_op!=NAT_FLAG_OP_TOUCH) + continue; + uint8_t xflags = gf & X_ALL; + if(xflags & (X_AF|X_PF)) // native NZCV cannot carry AF/PF at a fault + continue; + int vf_is_p = (dyn->insts[ninst].set_nat_flags&NF_PF_V)?1:0; + uint8_t need_nat = flag2native(xflags, vf_is_p); + if(!need_nat || (dyn->insts[ninst].set_nat_flags & need_nat) != need_nat) + continue; // producer must emit exactly the native flags needed + if(getNativeFlagsUsed(dyn, ninst, need_nat, vf_is_p) != need_nat) + continue; // every real consumer takes them natively, nothing else + // keep all NZCV flags this op sets alive so adjust_arch reconstructs ZF/SF/CF/OF + dyn->insts[ninst].need_nat_flags |= dyn->insts[ninst].set_nat_flags & (NF_EQ|NF_SF|NF_VF|NF_CF); + dyn->insts[ninst].x64.gen_flags = gf & ~X_PEND; + } } void rasNativeState(dynarec_arm_t* dyn, int ninst) diff --git a/src/include/env.h b/src/include/env.h index f2991d7b3d..6487db5fb2 100644 --- a/src/include/env.h +++ b/src/include/env.h @@ -63,6 +63,7 @@ extern char* ftrace_name; INTEGER(BOX64_DYNAREC_FORWARD, dynarec_forward, 128, 0, 1024, 1, 3) \ STRING(BOX64_DYNAREC_GDBJIT, dynarec_gdbjit_str, 0, 0) \ INTEGER(BOX64_DYNAREC_LOG, dynarec_log, 0, 0, 3, 1, 0) \ + INTEGER(BOX64_DYNAREC_LOOPFLAGS, dynarec_loopflags, 0, 0, 1, 1, 2) \ INTEGER(BOX64_DYNAREC_MISSING, dynarec_missing, 0, 0, 2, 1, 0) \ BOOLEAN(BOX64_DYNAREC_NATIVEFLAGS, dynarec_nativeflags, 1, 1, 1) \ STRING(BOX64_DYNAREC_NOHOSTEXT, dynarec_nohostext, 0, 0) \