#1156

VS2026 + xxh_x86dispatch.c is ~3x slower

Openshirouzuopened 1mo ago

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) streamingVS2026 (19.51) streamingVS2026 (19.51) one-shot
xxh_x86dispatch.c, default flags (picks AVX-512 on this CPU)69.6 GB/s21.6 GB/s70.3 GB/s
xxh_x86dispatch.c, -DXXH_DISPATCH_AVX2=0 -DXXH_DISPATCH_AVX512=0 (SSE2 kernel)—47.7 GB/s46.2 GB/s
single TU, XXH_AVX2, no /arch65.1 GB/s24.5 GB/s64.0 GB/s
single TU, XXH_AVX2, /arch:AVX264.5 GB/s64.1 GB/s64.6 GB/s
single TU, XXH_AVX512, no /arch—21.5 GB/s69.5 GB/s
single TU, XXH_AVX512, /arch:AVX2—70.2 GB/s69.3 GB/s
single TU, XXH_AVX2, no /arch, -DXXH3_STREAM_USE_STACK=024.6 GB/s23.7 GB/s64.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 back

    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 the acc_vec local of the inlined XXH3_scrambleAcc_avx2 (xxhash.h:5189). MSVC has already split the acc[8] array into two __m256i temporaries; 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:AVX or /arch:AVX2 likewise.

  • The dispatcher object (xxh_x86dispatch.c, default flags) shows the same pair in the second loop copy of XXH3_update_avx2 (vpaddq ymm2,ymm0,[rbp] / vmovdqu [rbp],ymm4) and of XXH3_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:AVX2 the 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.c with -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_avx2 and XXH3_update_avx512 keep their accumulators in registers across the stripe loop (they store to state->acc every 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 /MT or /MD choice as the rest of the build; it just takes no part in /GL link-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:AVX2 for 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_regular

It 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.c
  • XXH3_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 from cl /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 of xxh_x86dispatch.c (dumpbin /disasm), for the clang-cl workaround above.

xxhash_issue_attachments.zip

Activity

3
Cyan49731mo ago

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.

shirouzu1mo ago

Thank you. I'll give that a try.

shirouzu1mo ago