Repository navigation
Fix sha ni avx transition - #2083
Conversation
SHA-NI (sha1/sha256) instructions have only legacy SSE encodings, so executing them with dirty upper ymm/zmm state incurs an SSE/AVX transition penalty. sha::zeroupper() emits vzeroupper on AVX builds and is a no-op otherwise (including neon and no-sha builds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compilers emit vzeroupper only at call and return boundaries. When a native sha256/sha160 transform is inlined after a 256/512 bit write, every SHA-NI instruction pays the transition penalty (~100x slower). Call sha::zeroupper() once at the entry of each native transform, finalize and double hash sequence (not per round). sha512 native is VEX encoded and unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ablocks_t<one> temporary was copied with a zmm move ahead of the transform. On the native path, transform the block in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Single block accumulation preceded by a 256 bit store, for sha256 and sha160 across all dispatches. Reproduces the transition stall when the native transform is inlined (HAVE_PERFORMANCE_TESTS and HAVE_AVX2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks! A few things before this can go in. All the existing native benchmarks are unchanged within noise. The only large delta comes from the new test, which puts a 256-bit store before every accumulate inside a loop built to be inlined, and the standalone reproducer needs forced inlining parameters. That shows the mechanism exists, not that it happens in practice. You name muhash3072::flush() as an affected real path. Please post before/after numbers for it, or for some other real caller, measured on non-virtualized hardware. #ifdef in intel_sha.hpp. Raw intrinsics are wrapped in intel.hpp and gated at block level, with the alternative in #else. Please follow that pattern: sha::zeroupper() then just calls mm256_zeroupper() with no conditional. Comments. /// declaration docs should be one line, in the style of the sibling declarations. The paragraph on zeroupper() should be cut down to one line. Description. The testing section still has the TESTS_RESULT placeholder. The in-place native accumulate(state, block) change is fine on its own merits. |
Move HAVE_AVX gating into an mm256_zeroupper() wrapper in intel.hpp (empty under #else), so sha::zeroupper() calls it unconditionally. Reduce zeroupper() docs to one line and drop the explanatory comments in algorithm_native.ipp, accumulate and transition.cpp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Addressed in 54ba857:
muhash3072::flush() numbers. One caveat up front: I don't have non-virtualized hardware right now, so these are from a KVM guest (Xeon @ 2.10GHz, pinned, interleaved runs, medians of 31 rounds). Base is 1185c20, PR is 54ba857. In the base build,
To be clear about what this shows: the penalty in flush's hashing step is real, about 50 µs, but that's ~0.35% of a flush, which is dominated by the 3072-bit inverse. So there's no measurable end-to-end gain for The case for the change is that it costs one |
Replace raw _mm256_storeu_si256/_mm256_set1_epi32 calls with the library's f::store and f::broadcast<xint256_t> wrappers, consistent with other AVX2 tests and avoiding direct platform intrinsic usage in test code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9ff25e2 to
936d31b
Compare
Problem
SHA-NI instructions (
sha256rnds2,sha256msg1/2,sha1rnds4, ...) only have legacy SSE encodings. If they run while the upper ymm/zmm state is dirty, each one pays an SSE/AVX transition penalty. Compilers emitvzeroupperonly at call and return boundaries. So when a native sha256/sha160 transform is inlined into code that has just written a 256/512-bit register, nothing clears that state.Whether this happens depends on inlining decisions, and those change with unrelated edits to the surrounding code. Two real triggers:
accumulate(state, block)copies the block into anablocks_t<one>temporary using azmmmove.accumulate(state, iblocks)and thenvector.push_back(state): GCC copies the 32-byte state with aymmmove.In the baseline static library,
muhash3072::flush()is affected. Header-inlined use in downstream libraries can be affected too.Changes
HAVE_AVXandsha::zeroupper(). On AVX builds this emitsvzeroupper. It is a no-op on non-AVX x86, NEON and no-SHA builds.accumulate(state, block)now transforms the block in place instead of copying it intoablocks_t<one>.hash_performance__sha{256,160}__dirty_upper_64. It does a 256-bit store before each single-block accumulate.Results
GCC 13.3,
-O3 -msha -mavx2 -mavx512f -mavx512bw -mavx512vl -mavx512ifma, Xeon @ 2.1 GHz (AVX-512/AMX, virtualized). Median of 5. Each before/after cell shows two separate runs.Dirty upper state (new test), ns/block
Existing hash benchmarks: no regression (within noise)
Codegen (static library, objdump)
vzeroupperA standalone reproducer forces the transform inline with
--param max-inline-insns-*. Before the fix, every case stalls at 5.5–8.3 µs/block, includingaccumulator::write(usually ~57 ns/block). After the fix, every case runs at 50–75 ns/block.Testing
libbitcoin-system-testsuite at 54ba857 (make check, GCC 13.3, Boost 1.86,--enable-shani --enable-avx2 --enable-sse41 --enable-ndebug, static):HAVE_PERFORMANCE_TESTSdefined, including the newhash_performance__sha{256,160}__dirty_upper_64; native, vector and scalar results agree.-mavx2 -msha -msse4.1,-msha -msse4.1(no AVX) and no flags:vzeroupperis emitted only when AVX is enabled, and SHA-NI codegen is otherwise unchanged.sha::zeroupper()is an empty inline there).