VS2026 (MSVC 19.51): xxh_x86dispatch.c is ~3x slower(Zen5) - one accumulator of the stripe loop is kept on the stack
Summary
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.c with 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_update keeps 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's XXH3_update_avx2 / XXH3_update_avx512 (there the single __m512i accumulator 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.
build (all /O2 /MT) | VS2022 (19.44) streaming | VS2026 (19.51) streaming | VS2026 (19.51) one-shot |
|---|---|---|---|
xxh_x86dispatch.c, default flags (picks AVX-512 on this CPU) | 69.6 GB/s | 21.6 GB/s | 70.3 GB/s |
xxh_x86dispatch.c, -DXXH_DISPATCH_AVX2=0 -DXXH_DISPATCH_AVX512=0 (SSE2 kernel) | — | 47.7 GB/s | 46.2 GB/s |
single TU, XXH_AVX2, no /arch | 65.1 GB/s | 24.5 GB/s | 64.0 GB/s |
single TU, XXH_AVX2, /arch:AVX2 | 64.5 GB/s | 64.1 GB/s | 64.6 GB/s |
single TU, XXH_AVX512, no /arch | — | 21.5 GB/s | 69.5 GB/s |
single TU, XXH_AVX512, /arch:AVX2 | — | 70.2 GB/s | 69.3 GB/s |
single TU, XXH_AVX2, no /arch, -DXXH3_STREAM_USE_STACK=0 | 24.6 GB/s | 23.7 GB/s | 64.8 GB/s |
Intel 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_AVX2 31.8 (19.44) / 20.9 (19.51) / 31.3 (19.51 with /arch:AVX2); XXH_AVX512 18.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 /arch and 59.0 with /arch:AVX2 on the Zen 5 machine, i.e. no regression.
Where the accumulator lives
From cl /O2 /MT /FAs of the attached repro (listings attached), XXH3_update_regular under 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:
vpaddq ymm2, ymm0, YMMWORD PTR acc_vec$2$[rbp] ; second accumulator reloaded ... vmovdqu YMMWORD PTR acc_vec$2$[rbp], ymm4 ; and stored backwhile 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 copy acc[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=0 the loop accumulates through state->acc in 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 __m256i locals 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 into state->acc, it stores both accumulators every stripe but never reloads them inside the stripe loop, and measures 55.8 GB/s with /arch:AVX2 on Zen 5.
Why /arch is not an option for the dispatcher
As the file header already says, the dispatcher TU must be built with default flags.
Building it with /arch:AVX2 does 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 after XXH_featureTest correctly returns XXH_SSE2 on 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
- Build
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. - Compile that one file with clang-cl (the dispatcher supports it through per-function target attributes). With clang-cl 22.1.3 from the VS2026 LLVM toolset the dispatcher measures 67.1 GB/s streaming on Zen 5 (MSVC 19.44: 69.6, MSVC 19.51: 21.6) and 32.8 GB/s on Sapphire Rapids (MSVC 19.44: 32.5, MSVC 19.51: 18.5); both
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. - Stay on the 14.44 toolset for that file.
- What we do in our own project (FastCopy) : one .cpp per ISA, each compiled with its own
/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_sw
Repro
msvc_avx_repro.c attached; drop it next to xxhash.h (and xxh_x86dispatch.c/.h) in the repo and run from an x64 Native Tools prompt:
cl /O2 /MT msvc_avx_repro.c && msvc_avx_repro.exe
cl /O2 /MT /arch:AVX2 msvc_avx_repro.c && msvc_avx_repro.exe
cl /O2 /MT /DUSE_DISPATCH msvc_avx_repro.c xxh_x86dispatch.c && msvc_avx_repro.exe
cl /O2 /MT /FAs msvc_avx_repro.c # listing: XXH3_update_regularIt prints streaming and one-shot throughput and the full 128-bit hash. xxhsum -b built with DISPATCH=1 on the two toolsets should show the same thing on the XXH3_stream / XXH128_stream lines 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.
Activity
3Thank 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
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.