[ARM64_DYNAREC] Skip dead scalar SSE upper-lane preserve - #4143
Conversation
|
Overall, I don't understand how it works. math operationo SD are supposed to preserve the upper 64bits part, while ARM64 operation will set the upper part to 0. The only way to know if a math operation is safe to do without preserving the upper 64bits is if the register has been loaded with a MOVSD. |
597e063 to
a774255
Compare
The proposed change reuses box64's existing upper-lane liveness rather than introducing a new heuristic. box64 already tracks the AVX The in-place path (which zeroes the upper) is taken only when the upper-64 is provably dead: no reachable instruction reads the full register before the lane is overwritten. It's the same backward propagation as whole-xmm; the only seeding difference is: xmmh_used = xmm_used & ~xmm_scalar;A scalar SD op is flagged in
The upper is preserved, not zeroed. That SQRTSD only gets the in-place path if if ((barrier & BARRIER_FLOAT) || (jmp && jmp_insts == -1)) {
...
xmmh_needed = 0xffff; // every upper lane pinned live at an unresolved exit
}so the lane is live across the exit and stays preserved. A lone/leading SQRTSD therefore keeps the incoming upper. The MOVSD-loaded case you mention is subsumed: the load makes the old upper dead, exactly like any later overwrite. If a full-width read is reachable, the lane is never zeroed. |
|
nope, there is still I don't get (but I appreciate that you reused existing mecanism). I still don't understand why MOVSD & SQRTSD have basicaly the same change, while one is a load that should start a chain, while the second is an operation that can benefit from the chain. Can you do some dumps with your sample case so I understand better the effects in the code? |
|
Also, side not: what about the |
|
Also also, thos new xmmh_used & friend should be printed in dump (on |
This tracks liveness for the dead upper lanes of the low XMM registers
separately from whole-register XMM liveness: an upper-64 lane for the
scalar-double ops and an upper-96 lane for the scalar-single ops. Scalar
operations mark their operands as not genuinely reading those upper bits,
while barriers and unknown exits keep the conservative all-needed
behavior.
Use that liveness in the ARM64 F2 0F scalar-double lowering to compute
SQRTSD, ADDSD, MULSD, SUBSD, and DIVSD directly in the destination when
the destination upper-64 is proven dead, and in the F3 0F scalar-single
lowering for SQRTSS, ADDSS, MULSS, SUBSS, and DIVSS when the destination
upper-96 is proven dead. The existing preserve path remains the fallback
for live or unknown upper lanes and for FASTNAN-disabled paths. MOVSS
store and the other scalar-single ops mark their low-32 operands so a
stored scalar result does not pin the upper lane live (mirroring MOVSD
store, which was the difference that let the scalar-double path fire
while the scalar-single one did not).
Benchmark: static x86 benchfloat (LINPACK) on an Ampere eMag, n=9
core-pinned runs, BOX64_DYNACACHE=0 to defeat the on-disk dynarec
cache, parent e3161999c built as the "before". Median KFLOPS:
double -O2 (scalar-double path)
SAFEFLAGS=1 : 637,631.540 -> 700,135.472 +9.8% (MAD 226.468)
SAFEFLAGS=0 : 765,920.767 -> 853,745.312 +11.5% (MAD 936.058)
single -O2 (scalar-single path)
SAFEFLAGS=1 : 513,127.344 -> 611,984.438 +19.3% (MAD 264.312)
SAFEFLAGS=0 : 566,161.312 -> 696,077.312 +23.0% (MAD 65.313)
Host: Ampere eMag; 1 socket x 32 cores x 1 thread; up to 3.0 GHz; Arm64
Linux v6.8.0.
a774255 to
f7553ab
Compare
MOVSD isn't given the skip. Only the arithmetic ops (SQRTSD/ADDSD/MULSD/SUBSD/DIVSD) get the in-place Your "chain" intuition is right, just backward. The chain isn't started by a load; it's the backward dead-upper liveness seeded from wherever the upper is next fully overwritten. A MOVSD load from memory does zero the upper (a full
Dumps from a tiny sample (using the new Upper dead — SQRTSD followed by a MOVUPD that fully overwrites xmm3:
Upper live — SQRTSD whose upper is then read by UNPCKHPD:
|
DONE. It needs its own upper-96 lane: The unlock was exactly the store point from the other thread: MOVSS store was not marking its operand scalar the way MOVSD store already does. Without it, storing a scalar-single result pinned the upper lane live, so it never fired in real Benchmark — static x86 benchfloat (LINPACK), Ampere eMag, n=9 core-pinned, (the scalar-double path on the same host was +9.8% / +11.5%.) SSE/x87/FPU ctests pass; a dump of SQRTSS followed by an ADDSD reading bits[0:63] confirms it keeps the preserve (upper-64 dead, upper-96 live) — the case the separate |
DONE. if (n.xmmh_used || n.xmmh_needed || n.xmmh_unneeded)
length += sprintf(buf + length, " xmmHused=%04x/needed=%04x/unneeded=%04x",
n.xmmh_used, n.xmmh_needed, n.xmmh_unneeded);plus an |
Ah ok. I see now in what cases the xmmh_unneeed triggers! thanks for the detail explanation. I'm currently preparing the next release on box64, so I will keep this PR open until the start of the next developement cycle after the release (should happens this week hopefully). Side note: I was initialy reviewing this opcode with my initial idea for this in mind: to use a forward chain starting at with a |
This tracks liveness for the dead upper lanes of the low XMM registers separately from whole-register XMM liveness: an upper-64 lane for the scalar-double ops and an upper-96 lane for the scalar-single ops. Scalar operations mark their operands as not genuinely reading those upper bits, while barriers and unknown exits keep the conservative all-needed behavior.
Use that liveness in the ARM64 F2 0F scalar-double lowering to compute SQRTSD, ADDSD, MULSD, SUBSD, and DIVSD directly in the destination when the destination upper-64 is proven dead, and in the F3 0F scalar-single lowering for SQRTSS, ADDSS, MULSS, SUBSS, and DIVSS when the destination upper-96 is proven dead. The existing preserve path remains the fallback for live or unknown upper lanes and for FASTNAN-disabled paths. MOVSS store and the other scalar-single ops mark their low-32 operands so a stored scalar result does not pin the upper lane live (mirroring MOVSD store, which was the difference that let the scalar-double path fire while the scalar-single one did not).
Benchmark: static x86 benchfloat (LINPACK) on an Ampere eMag, n=9 core-pinned runs, BOX64_DYNACACHE=0 to defeat the on-disk dynarec cache, parent e3161999c built as the "before". Median KFLOPS:
Host: Ampere eMag 8180; 1 socket x 32 cores x 1 thread; up to 3.0 GHz; Arm64 Linux v6.8.0.