[ARM64_DYNAREC] Reduce default safe-flag overhead with native flag carry - #4154
[ARM64_DYNAREC] Reduce default safe-flag overhead with native flag carry#4154jserv wants to merge 2 commits into
Conversation
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.
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.
|
So, can you split this PR in 2 (or 3 part), with the simple stuffs separated from the LOOP thingy (and the xZR stuff alone maybe, as this would be a 0 risk PR). About the LOOP thing, I suppose you are using some form of Coding Agent? Because every explanation is ultra verbose and yet not really clear (sorry). If I understand correctly, the idea here is to ignore AF &PF flags when most other flags are already computed, as native, and X_PEND is also required: the idea is then to remove X_PEND and just ignore AF & PF to get faster loop (altho no loop check is involved here) by not storing Defered Flags informations. It's an interesting idea, but I want to point out that, for some opcode like COMI, PF is actually computed using native VF flags. Also, the name LOOP while it's not testing about loop is a bit surprising. Also also, there are documentation to change when you add a new env. var ( Last point: I'm working a creating a new release of box64 (v0.4.4), so this PR will have to wait for the next developement cycle anyway. |
Summary
Two ARM64 dynarec changes that cut the flag-handling overhead box64 pays under the default safe-flags policy (
BOX64_DYNAREC_SAFEFLAGS=1), without changing correctness for the common case.TEST/CMP emission cleanup. Route
TEST Eb,imm8(0xF6 /0) throughemit_test8c, store{x,w}ZRinto the deferredop2ofemit_cmp{32,16,8}_0instead of a zeroed scratch, and add ZF/PEND-only fast paths toemit_test{16,16c,8c}that use unshiftedANDS/ANDSw_mask. Static-only cleanup, runtime unchanged.Native flag carry across block boundaries. A flag producer that ends a block has
X_PENDforced onto it, so the deferredop1/op2/resspill 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 consumers purely through native ARM NZCV, as in the common loop-controlCMPfeeding a nativeJcc, that spill is redundant for the branch. A guarded pass inupdateNativeFlags(reusinggetNativeFlagsUsedfor the coverage proof) clearsX_PENDfor those producers, soemit_cmp/emit_testtake theSET_DFNONEpath. Controlled by a newBOX64_DYNAREC_LOOPFLAGSenv var.Result
On Ampere eMag 8180,
benchfloat-O2LINPACK, default safe flags, n=15 alternating, core-pinned,BOX64_DYNACACHE=0:+20.2%, the two distributions fully disjoint. The optimized figure matches
SAFEFLAGS=0(about 763k), so it recovers the entire default safe-flag gap on this workload.The hot
daxpy_urloop-controlCMPdrops from 6 emitted ARM ops (STR op1/op2/df/res+SUBS) to 1 (SUBS); theJNZstill branches on native NZCV.The TEST/CMP cleanup on its own is a static win only:
benchfloat-O2native instruction count 85,587 to 85,070 (517 fewer, about 68% of the 755 instruction safe vsSAFEFLAGS=0gap), with no runtime change (the removed ops are cheap MOV/shift ALU the out-of-order core hides).Correctness
All under default safe flags, on the same eMag host:
ctest34/34, includinglongjumpInSignalstest05-O2output bit-identical to baselineDYNAREC=0interpreterBOX64_DYNAREC_TEST): zero divergence, correct outputZF/SF/CF/OFstay reconstructable at faults throughadjust_arch's NZCV overlay (kept inneed_nat_flags). Only PF/AF turn approximate across the block exit, which the dynarec already is for native flags (only the interpreter is exact at async fault points), and which the x86 ABI (arithmetic flags not preserved across calls) makes safe. A mid-block nativeCMP+Jccalready emits no deferred spill (gen_flags=X_ZF, noX_PEND); this brings the same treatment to the block-boundary case.Notes
BOX64_DYNAREC_LOOPFLAGSdefaults to0. The safety argument for clearing the boundary spill rests on x86 ABI flag-death across calls, so it is kept opt-in pending broader real-application and Wine validation.