Repository navigation
SSE2: keep i32 multiply products in place - #16
Merged
Merged
Conversation
clang 23 no longer recognises the psrldq/pmuludq/pshufd/punpckldq sequence as a 32 bit multiply, so chained multiplies (e.g. hash * hash * constant) repack the lanes in between. Combining the even/odd pmuludq results in place with and/shift/or keeps the low 32 bits of each 64 bit lane where pmuludq reads them: a single multiply is still 6 instructions, a chained one needs no repacking. The in-place and/shift/or form is as fast or faster than the shuffle/unpack repack on MSVC 14.51/14.44 and GCC 16/13 too (FastNoise2 SSE2 benchmarks 0.985-0.993 of the previous time), so every compiler uses it. Integer results are identical.
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
SSE2 has no 32-bit
mullo, soi32x4::operator*=(ToolSet/x86/128/i32x4.h) emulates it with twopmuludq. One handles lanes 0 and 2. The other handles lanes 1 and 3 after apsrldq. Twopshufdand apunpckldqthen pack the products back together.Clang 19 sees through this sequence. When multiplies are chained, it skips the repack between them. Clang 23 doesn't, so a chain like FastNoise2's value hash,
hash *= hash * A, now packs and unpacks the lanes after every multiply. On FastNoise2 master, SSE2 Value noise is 14-22% slower with clang-cl 23 than with clang-cl 19.Change
pshufd( 3, 3, 1, 1 )instead ofpsrldq.or( and( mul20, low 32 bits of each 64-bit lane ), slli_epi64( mul31, 32 ) ).pmuludqonly reads the low 32 bits of each 64-bit lane, and the and/shift/or leave each product there. So a following multiply can use them without repacking. A multiply by a constant is still 6 instructions. The result is the same in every lane.I first did this for clang only. It's as fast or faster with MSVC and GCC too, so every compiler now uses the same code. SSE4.1 and up use
_mm_mullo_epi32and don't change.Effect
Instruction counts for a single
a * band for the chainedh * ( h * A ). These are whole test functions that load their inputs and store the result, so they include those loads, the store and the constant:GCC 16 builds the and mask in general purpose registers instead of loading it. In real code that happens once, outside the loop. With clang-cl 23, FastNoise2's SSE2 Value
Gengets about 14% shorter: 341 → 291, 593 → 505 and 1038 → 895 instructions for 2D, 3D and 4D.I built FastNoise2 master (8c2d080) against this branch and against FastSIMD master, then timed both on a Ryzen 9 7950X. Both builds are loaded in one process and timed interleaved on one pinned core. Each figure is the median of 3 processes with alternating order, over the default 63 node/dimension cases. The SSE4.1 code is instruction-identical in both builds, so it serves as a control. The numbers are new/old time:
Single cases above 1.03 come from code placement, and they differ from compiler to compiler. Single SSE4.1 cases, whose code doesn't change, measured anywhere from 0.75 to 1.55. As a check, I rebuilt both sides with
-falign-functions=64and aligned loops, which moves the code around. A DomainWarpSimplex 4D case went from 1.22 to 0.98 (Linux clang 23) and from 1.12 to 0.98 (GCC 16), while the Value and White gains stayed the same.Testing
i32 multiply operator,i32 masked mulandi32 inv masked mulon SSE2 and SSE2 RELAXED. Each runs at 128 bit and at 256, 512 and 1024 bit, which are built from the 128-bit register./fp:fastand-ffast-math, and the compilers reorder the float maths around the changed integer code. WithFASTNOISE2_STRICT_FP=ON, clang-cl 23 and MSVC 19.51 are bit-identical in all 1212 cases. The integer results don't change.i32x4.hand aren't affected.Other PRs
This doesn't touch the files changed by the other open performance PRs (the AVX-512 headers,
Generic/Functions.h, ARM and WASM), and it merges cleanly with each of them.