Repository navigation
MSVC AVX512: copy 512 bit registers and masks through their native type - #13
Merged
Merged
Conversation
MSVC lowers copies of trivially copyable 64 byte structs as two 32 byte stores,
which then get reloaded as one 512 bit register and stall store forwarding. Every
composite op (Register{ f( a.v0 ), f( a.v1 ) }), operator copy and assignment hit
it. A user-provided copy constructor and assignment that copy .native keep these
copies in zmm registers. MSVC only; clang and GCC keep the trivial copies.
MSVC writes copies of a 32 lane composite mask with two 16 bit stores and then reads both halves with one 32 bit load, which can't be store forwarded (FastNoise's cellular lane checks hit this twice per call). A user-provided copy constructor and assignment on the AVX512 mask keep the halves in registers. 32 lane AnyMask uses kortestw instead of moving both halves to general purpose registers. MSVC only.
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
MSVC copies a trivially copyable 64 byte struct with two 32 byte stores. When that copy is next read as one 512 bit register, the load can't be store forwarded and stalls. On AVX512
Register<float, 16>andRegister<int32_t, 16>are such structs, and FastSIMD copies them all the time: every composite op (Register{ f( a.v0 ), f( a.v1 ) }), operators that copy and then compound assign, and arguments passed by value. 32 lane composite masks have the same problem at a smaller size: two 16 bit stores, then one 32 bit load.As a result MSVC's AVX512 build of FastNoise2 is slower than its AVX2 build. With 19.51 on a grid it takes 1.15x the AVX2 time on average, and 34 of 63 benchmark rows are slower. clang and GCC don't split these copies.
Change
MSVC only (
_MSC_VER && !__clang__). clang-cl, clang and GCC compile the same code as before.f32x16.h,i32x16.h: a user-provided copy constructor and copy assignment that copy.native.mNx16.h: the same for the AVX512 mask, plus a 32 laneAnyMaskoverload that tests both halves with onekortestwinstead of moving each half to a general purpose register.Cost
The 512 bit registers are no longer trivially copyable on MSVC, so they stop being HVAs. MSVC now passes them to non-inlined functions by reference to a caller copy, not in zmm registers. In FastNoise2 that is the per-call
Genboundary, so a few very cheap nodes get slower (numbers below). FastNoise2 can avoid this on its side by passing generator arguments as a plain aggregate of the native halves. FastNoise2's performance branch does that, and there no row is slower.Measured
FastNoise2 master (8c2d080) built against this branch, compared with the same source built against FastSIMD master, on a Ryzen 9 7950X. Both builds were loaded in one benchmark process and timed interleaved on one pinned core, in 3 processes with the A/B order alternating, and took the median of the per-process ratios. The default case set has 63 rows per feature set (21 generators/trees x 2D/3D/4D), run on a uniform grid and on random positions. Time ratio, lower is faster:
--all) the geomean is 0.670. 23 rows are above 1.02, all of them 2D and mostly cheap combinator nodes such asOp_PowInt1.18,Op_RemoveDimension1.15 andOp_SeedOffset1.15. In those, passing arguments by reference costs more than the saved copies.vextract*64x4drops from 3102 to 659.Earlier numbers come from FastNoise2's performance branch, which uses composites more heavily. They were measured on a busy machine. The first commit alone gave 0.66 (19.51) and 0.71 (19.44) for the AVX512 geomean. The mask commit gave another 0.96 on 19.51 (0.82-0.89 on cellular 3D/4D). Together with the FastNoise2-side argument change, 19.51 AVX512 came to 0.61 of that branch's time, with no row slower.
Correctness
This change also happens to avoid two MSVC SSA optimiser bugs on structs of two
__m512i, which is what a 32 laneint32vis. I checked both with short repros on FastSIMD master'sFS::Register<int32_t, 32>/FS::Register<float, 32>(/O2 /arch:AVX512). Both build correctly with/d2SSAOptimizer-:seed ^ ( x ^ y ^ z )next toseed ^ x ^ y ^ z: 19.51 and 19.44 get the upper 16 lanes wrong. With this change, no lanes are wrong.( Convert( x ) ^ Convert( y ) ) & 1(FastNoise2's Checkerboard): 19.44 folds it into a wrongvpternlogdand gets 8 of 32 lanes wrong. With this change, none are wrong.These are compiler bugs and this only sidesteps them, but the effect is real. Under VS 2022, FastNoise2's Checkerboard AVX512 output is wrong on master and now matches clang-cl. In FastNoise2's performance branch, 19.51 produced garbage in about 140 AVX512 rows, and this change fixes them.
FastNoise2 output, comparing master + this branch with master + FastSIMD master on the same compiler:
/fp:fastcontracts more multiply-adds into FMAs (7846 -> 8008 in the AVX512 object). The output is not further from clang-cl's: 29 rows differ from it by more than 1e-3, against 32 before.Testing
FastSIMD_tests, Release, 4,194,304 random inputs per test, seeds 24301, 1 and 2:
m32 any maskandm32i any maskat 1024 bit cover the newAnyMaskoverload: changing it to ignore the upper half makes both fail.Related PRs
f32x16.h,i32x16.handmNx16.h. It merges cleanly with this one, but whichever goes in later may need a rebase.AnyMasktestv0 | v1. The MSVC overload here is more specialised, so MSVC AVX512 keepskortestw, which also ORs both halves.Registercopies (a defaultedFS_FORCEINLINEcopy constructor and assignment). That guards against MSVC calling those copies out of line in very large functions. FastNoise2 master doesn't hit that case, and its AVX512 code is identical with or without the guard. The guard also changes MSVC's SSE2 register allocation, which makes SSE2 Perlin 3D 1.3-1.4x slower on 19.51.