VS2026 + xxh_x86dispatch.c is ~3x slower #1156
Labels
No labels
announcement
bug
build
CI
dependencies
documentation
duplicate
enhancement
experience feedback
feature request
github_actions
help wanted
install
invalid
Long-term
optimization
portability
question
target-specific
test issue
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-Cyan4973/xxHash#1156
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
VS2026 (MSVC 19.51):
xxh_x86dispatch.cis ~3x slower(Zen5) - one accumulator of the stripe loop is kept on the stackSummary
I think this is an VS2026 code-generation regression (from VS2022), not an xxHash bug. I'm posting it here because users who build
xxh_x86dispatch.cwith the VS2026 currently get a dispatcher that is slower than the plain SSE2 kernel, and the dispatcher cannot use the obvious workaround (/arch).With VS2026 (cl 19.51.36257) and default flags, the second inlined copy of the stripe loop in
XXH3_updatekeeps one of the two 256-bit accumulators (the second half,acc[4..7]) in a stack slot and reloads/stores it every stripe. VS2022 (cl 19.44.35228) keeps both in registers. This happens both in a single-TU build (XXH_INLINE_ALL,XXH_VECTOR=XXH_AVX2) and in the dispatcher object'sXXH3_update_avx2/XXH3_update_avx512(there the single__m512iaccumulator is kept on the stack). Only the streaming API is affected;XXH3_128bits()is unchanged.Measurements
xxHash v0.8.4. AMD Ryzen Threadripper PRO 9975WX (Zen 5), Windows 11, x64. 16 MiB, best of 5 trials × 128 reps, thread pinned to one core; run-to-run spread is a few percent. Full 128-bit hash values are identical in every row.
/O2 /MT)xxh_x86dispatch.c, default flags (picks AVX-512 on this CPU)xxh_x86dispatch.c,-DXXH_DISPATCH_AVX2=0 -DXXH_DISPATCH_AVX512=0(SSE2 kernel)XXH_AVX2, no/archXXH_AVX2,/arch:AVX2XXH_AVX512, no/archXXH_AVX512,/arch:AVX2XXH_AVX2, no/arch,-DXXH3_STREAM_USE_STACK=0Intel Xeon w5-3425 (Sapphire Rapids), same binaries, streaming: dispatcher 32.5 GB/s (19.44) → 18.5 GB/s (19.51); dispatcher with AVX2/AVX-512 disabled (SSE2 kernel) 22.4; single TU
XXH_AVX231.8 (19.44) / 20.9 (19.51) / 31.3 (19.51 with/arch:AVX2);XXH_AVX51218.6 (19.51, no/arch).One-shot is 31–33 GB/s in every build. So on Intel too the regressed dispatcher ends up slower than the plain SSE2 kernel.
Only x64 is affected: the same repro built for x86 with 19.51 measures 58.0 GB/s streaming without
/archand 59.0 with/arch:AVX2on the Zen 5 machine, i.e. no regression.Where the accumulator lives
From
cl /O2 /MT /FAsof the attached repro (listings attached),XXH3_update_regularunder 19.51 without/arch:The function contains two inlined copies of the stripe loop (up-to-block-boundary, and whole-blocks). The first keeps both accumulators in registers. The second — the hot one — has, per stripe:
while the first accumulator stays in
ymm6. ymm5 is not referenced anywhere in that loop, so this is not register pressure.The slot is not
acc[]itself: it is the home of theacc_veclocal of the inlinedXXH3_scrambleAcc_avx2(xxhash.h:5189). MSVC has already split theacc[8]array into two__m256itemporaries; 19.51 just leaves one of them in memory across the loop.19.44's listing has no such slot; both accumulators are in
ymm5/ymm6. 19.51 with/arch:AVXor/arch:AVX2likewise.The dispatcher object (
xxh_x86dispatch.c, default flags) shows the same pair in the second loop copy ofXXH3_update_avx2(vpaddq ymm2,ymm0,[rbp]/vmovdqu [rbp],ymm4) and ofXXH3_update_avx512(vpaddq zmm2,zmm1,[rbp+40h]/vmovdqu32 [rbp+40h],zmm2) under 19.51, and none under 19.44.Sampling the running thread's instruction pointer confirms it: 64% of samples land on the instruction right after the reload (skid), 86% inside that loop; with
/arch:AVX2the samples spread evenly over the loop.About
XXH3_STREAM_USE_STACK: the accumulators reach the loop through the stack copyacc[8](xxhash.h:6770, introduced in 2021 — for #516, I believe). That copy is what lets the compiler promote them to registers at all: with-DXXH3_STREAM_USE_STACK=0the loop accumulates throughstate->accin memory on both MSVC versions and both are slow (24.6 / 23.7 GB/s). So the 2021 change is not the trigger; 19.51 simply no longer promotes one of the two temporaries in the second loop copy.I have not found a source-level shape that changes this (an explicit pair of
__m256ilocals is what MSVC already produces internally).For clang the setting does not exist — xxhash.h leaves the stack copy off for it (
!defined(__clang__)) — and clang-cl 22.1.3 indeed does not need it: accumulating straight intostate->acc, it stores both accumulators every stripe but never reloads them inside the stripe loop, and measures 55.8 GB/s with/arch:AVX2on Zen 5.Why
/archis not an option for the dispatcherAs the file header already says, the dispatcher TU must be built with default flags.
Building it with
/arch:AVX2does restore the speed here, but the object then contains AVX2 code in the SSE2 variants (XXH3_update_sse2: 249 VEX instructions / 30 ymm references;XXHL128_default_sse2: 139 / 16; both 0 with default flags), so it would fault (#UD) right afterXXH_featureTestcorrectly returnsXXH_SSE2on a pre-AVX2 CPU.I deduced this from
dumpbin /disasm; I don't have such a machine to confirm.Workarounds available to MSVC 19.51 users today
xxh_x86dispatch.cwith-DXXH_DISPATCH_AVX2=0 -DXXH_DISPATCH_AVX512=0: the SSE2 kernel is 2x faster than the regressed path on Zen 5 (47.7 vs 21.6 GB/s) and 1.2x on Sapphire Rapids (22.4 vs 18.5). It gives up what the dispatcher is for, but it is not slower.XXH3_update_avx2andXXH3_update_avx512keep their accumulators in registers across the stripe loop (they store tostate->accevery stripe but never reload there), and the hash output matched the MSVC builds bit for bit. The object links into an MSVC build like any other COFF object (C linkage, same/MTor/MDchoice as the rest of the build; it just takes no part in/GLlink-time code generation and needs the LLVM/clang-cl component of the VS installer). This is the best workaround I found./arch(/arch:AVX2for both the AVX2 and the AVX-512 file), selected at runtime by CPUID. Under 19.51 that measures 64–70 GB/s on this CPU. I mention it as a data point, not as a proposal for upstream, since it changes the build for every user. https://github.com/shirouzu/xxh3_swRepro
msvc_avx_repro.cattached; drop it next toxxhash.h(andxxh_x86dispatch.c/.h) in the repo and run from an x64 Native Tools prompt:It prints streaming and one-shot throughput and the full 128-bit hash.
xxhsum -bbuilt withDISPATCH=1on the two toolsets should show the same thing on theXXH3_stream/XXH128_streamlines with a large-B; I have not run xxhsum myself.Attachments
msvc_avx_repro.cXXH3_update_regular_19.44_noarch.asm,XXH3_update_regular_19.51_noarch.asm,XXH3_update_regular_19.51_archAVX.asm,XXH3_update_regular_19.51_archAVX2.asm,XXH3_update_regular_19.51_noarch_AVX512.asm— the function extracted fromcl /O2 /MT /FAs [flags] msvc_avx_repro.c.XXH3_update_avx2_clang-cl_22.1.3_dispatcher.txt,XXH3_update_avx512_clang-cl_22.1.3_dispatcher.txt— the same two functions from a clang-cl build ofxxh_x86dispatch.c(dumpbin /disasm), for the clang-cl workaround above.xxhash_issue_attachments.zip
Could it be worth a notification to Microsoft's VS Studio team?
The investigation is well detailed, they should be able to understand the issue and fix it generically in VS2026.
Thank you. I'll give that a try.
I reported it to MS. I hope they fix the regression.
https://developercommunity.visualstudio.com/t/Regression-VS20221944-%E2%86%92-VS20261951/11155787