Repository navigation
Composite AnyMask: test the OR of both halves - #15
Merged
Merged
Conversation
AnyMask( v0 ) || AnyMask( v1 ) compiles to two tests and two branches on SSE/AVX2 (and kunpck + kmovd + test on some AVX512 sites). Both halves are already computed, so OR them and test once.
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.
Change
AnyMaskfor composite registers (Generic/Functions.h, anything wider than the native register) wasAnyMask( a.v0 ) || AnyMask( a.v1 ). It is nowAnyMask( a.v0 | a.v1 ): one OR of the halves, then one native test. Wider composites recurse through the composite|, so a 1024 bit mask on SSE is three ORs and a singlemovmskps.The result is the same for any input, not only well-formed masks. Every native
AnyMaskeither looks for any set bit (AVX512, NEON, AARCH64, WASM, scalar) or for any set sign bit (movmskps,pmovmskb), and OR keeps both.Why
||short-circuits, so compilers test and branch on each half: twomovmskps+test+jneon SSE and AVX2, twokmov+teston AVX512, and a horizontal reduction and branch per half on the NEON level. Both halves are already in registers, so one OR (orps,korw,vorr) and one test is cheaper and leaves one branch instead of two.Instruction counts in FastNoise2's per feature set objects, before → after, clang-cl 23:
movmskps/kmovdjnemovmskps/kmovdjneMSVC 19.51 (master) goes from 51 to 26
(v)movmskpson each of SSE2, SSE4.1 and AVX2. For ARM (clang 23, compile only) the NEON level object shrinks from 53,491 to 53,418 instructions, and the AARCH64 level object doesn't change.Speed
FastNoise2 master only calls
AnyMaskin two nodes:GeneratorCache, andTerracewith a smoothness generator. So on master this is mostly an A/A check. I built FastNoise2 master (8c2d080) against this branch and against FastSIMD master and timed them with its benchmark harness on a Ryzen 9 7950X (one pinned core), and on an Apple M1. Each figure is the median of 3 processes with alternating order, over the default 63 node/dimension cases per feature set. Numbers are new/old time:Only the
GeneratorCacheandTerracefunctions change, andGeneratorCacheon its own is 1.01 (clang-cl) and 1.00 (MSVC) over 9 processes. A few rows move by more than 3% in both directions, but their code is identical, so that is code placement.FastNoise2's performance branch uses
AnyMaskin hot loops: the cellular cell checks, the Simplex/SuperSimplex vertex skips, the domain warp checks and the grid loop. Same method, built against FastSIMD master and this branch:Demo_MountainTerrain4D 0.90 (NEON, both compilers) and 0.91 (AARCH64, Apple clang), SuperSimplex 4D 0.96 (NEON), domain-warped SuperSimplex 3D 0.95-0.96. No row is slower than 1.02. NEON's SuperSimplex 4DGenFndrops from 2429 to 2367 instructions.CellularLookupAVX512 2D is 0.88. GCC 16 is slower on 2D cellular (1.03-1.075 on SSE2/SSE4.1/AVX2, where it spills a little more) and faster on SuperSimplex SSE2 4D (0.87). The MSVC SSE numbers come from three runs on a machine that was busy at the time.CellularDistanceAVX2 2D 0.94) and MSVC 19.51CellularLookupAVX512 2D 0.90.MSVC AVX512: merge after the native copies PR
Without #13, this change makes MSVC AVX512 slower wherever a 32 lane
AnyMasksits on a hot, hard to predict branch. On the performance branch, SuperSimplex 4D is 1.08 (19.51) and 1.17 (19.44), with all 5 processes agreeing, and FBm/Mountain 4D are 1.02-1.15. The function itself has fewer instructions (2670 → 2565) and fewer stack accesses.The cause is the MSVC split copy that the native copies PR fixes. MSVC stores a 32 lane mask (two
__mmask16) with two 16 bit stores, then reads it back with one 32 bit load, which can't be store forwarded. The||form tested the low half straight from its register and usually skipped the reload. The OR always waits for it, and the branch resolves later.#13 keeps masks in registers and adds a 32 lane
kortestwoverload. That overload is more specialised than the generic composite one, so it also ORs both halves before the test. With that PR applied, the MSVC AVX512 object is instruction-identical with or without this one, for 19.51 and 19.44 (0 of about 2470 functions change), and SSE/AVX2 change as above. FastNoise2 master isn't affected either way (MSVC 19.51 AVX512 1.000 above).Correctness
FastNoise2 output, comparing the build against this branch with the build against FastSIMD master on the same compiler:
Testing
m32 any maskandm32i any maskcover the change on every level wider than its native register, and on SCALAR (the reference) at every width. As a check, I changed it toAnyMask( a.v0 ). Both tests then fail at 128, 256, 512 and 1024 bit on every x86 level (clang-cl 23).Other PRs
AnyMaskoverload that is exactlyAnyMask( a.v0 | a.v1 ). Once this PR is in, that overload is redundant: the NEON and AARCH64 objects are identical with and without it (clang 23). Whichever of the two merges second can drop it.Generic/Functions.h. It merges cleanly with this one, and so do Route MaskedIncrement/MaskedDecrement through MaskedSub/MaskedAdd #14 and SSE2: keep i32 multiply products in place #16 (checked withgit merge-tree).