Repository navigation
x86: faster per-lane variable Shl for 8-bit lanes on AVX2/SSSE3/SSE4 - #3523
mvanslobbe wants to merge 5 commits into
Conversation
Below AVX3_DL, Shl of uint8_t widened to 32-bit lanes for vpsllvd (AVX2) or emulated the shift via 16-bit lanes (SSSE3/SSE4), then narrowed back: 32 instructions with cross-lane shuffles on AVX2. Instead, look up 1 << bits[i] with one TableLookupBytes and use two 16-bit multiplies. The low byte of a 16-bit product depends only on the low bytes of its inputs, so it is already the even lane's result; the odd lane is isolated in the high byte and multiplied by its own power of two. Shift counts are clamped to 8, whose table entry is 0, so counts >= 8 still return 0 as before. Throughput on an i7-12700H improves 4.9-5.9x on AVX2 and 3.2-3.8x on SSSE3 with clang 14-22 and gcc 13-17. Adds an exhaustive 8-bit Shl test.
jan-wassenberg
left a comment
There was a problem hiding this comment.
Nice find, congrats! Some minor improvements below.
Also, if you also want to tackle Shr, I think that would work in a similar way:
kShr[b] = 0x80 >> b ({0x80, 0x40, ..., 0x01, 0, ...}), then a 16-bit right shift by 7 ((v * (0x80 >> b)) >> 7, with 255 * 128 = 32640 < 32768 avoiding even signed int16_t overflow).
| return TruncateTo(d, PromoteTo(du16, v) << PromoteTo(du16, bits)); | ||
| } | ||
| #elif HWY_TARGET <= HWY_AVX2 | ||
| template <class V, HWY_IF_V_SIZE_LE_V(V, 8)> |
There was a problem hiding this comment.
I think this PromoteTo codepath is still good for V_SIZE_LE_V(V, 4), because that fits in one XMM; let's keep it.
There was a problem hiding this comment.
Agreed, done in dfbe28a. On AVX2, vectors of up to 4 bytes use the u32 PromoteTo path again, and 8 bytes and up use the multiply path. llvm-mca agrees: about half the uops and latency (e.g. Skylake 4 uops / 7 cycles vs 8 / 12).
| // inputs, so it is already the even lane's result. | ||
| const VW evens = Mul(vw, pow2); | ||
| // Isolate the odd lane in the high byte and multiply by its own 1 << bits. | ||
| const VW odds = Mul(And(vw, Set(dw, 0xFF00)), ShiftRight<8>(pow2)); |
There was a problem hiding this comment.
How about using a single 0x00FF constant for OrAnd and using AndNot for the inverse?
There was a problem hiding this comment.
Done in 248372b: one 0x00FF constant, shared by AndNot for the odd bytes and by OrAnd.
| for (size_t first_value = 0; first_value < 256; ++first_value) { | ||
| for (size_t first_shift = 0; first_shift < 8; ++first_shift) { | ||
| const Vec<D> values = | ||
| BitCast(d, Iota(RebindToUnsigned<D>(), first_value)); |
There was a problem hiding this comment.
Would be good to decorrelate odd and even entries - maybe Xor odds with random bits (Random64).
There was a problem hiding this comment.
Done in 07052bb. Odd lanes' values are now XORed with random bits, so the two bytes of each 16-bit multiply get independent inputs. The even lane still covers all 256 values × 8 counts.
| const V pow2_table = Dup128VecFromValues(d, 1, 2, 4, 8, 0x10, 0x20, 0x40, | ||
| 0x80, 0, 0, 0, 0, 0, 0, 0, 0); | ||
| const VW pow2 = | ||
| BitCast(dw, TableLookupBytes(pow2_table, Min(bits, Set(d, 8)))); |
There was a problem hiding this comment.
I think we can skip the Min() - previously, we were not clamping either.
There was a problem hiding this comment.
Done in b5d3457. I dropped the Min() and updated the comment, since counts of 8 or more are implementation-defined.
The promoted vector still fits in one XMM register, so vpsllvd is cheaper than the two 16-bit multiplies.
Isolate the odd lanes with AndNot instead of loading a second 0xFF00 mask.
Counts outside [0, 8) give implementation-defined results, and the previous code did not clamp them either.
Xor the odd lanes' values with random bits, so that each 16-bit pair holds independent inputs.
|
Thanks for the review! Good idea on Shr. I'll send that as a follow-up PR. The bazel failure looks unrelated: |
jan-wassenberg
left a comment
There was a problem hiding this comment.
Thanks for updating, LGTM :)
Below AVX3_DL,
Shl(Vec<uint8_t>, Vec<uint8_t>)widens to 32-bit lanes forvpsllvd(AVX2) or emulates the shift via 16-bit lanes (SSSE3/SSE4), then narrows back. On AVX2 this is 32 instructions including cross-lane shuffles.This change computes
1 << b[i]with oneTableLookupBytes, then uses two 16-bit multiplies. The low byte ofa16 * pow2depends only on the low bytes of its inputs, so it is already the result for the even lanes. For the odd lanes, the odd byte ofa16is isolated and multiplied bypow2 >> 8, so the high byte of that product is the result.OrAndmerges the two: 12 instructions on AVX2, no cross-lane operations.Shift counts are clamped with
Min(b, 8)and table entry 8 is 0, so counts >= 8 still return 0, as the current AVX2 and SSSE3 paths do. Without the clamp,pshufbwould only use the low 4 bits of counts 16..127.AVX3_DL (
gf2p8mul), AVX3 (vpsllvw) and SSE2 are unchanged.Results
Throughput, picoseconds per byte, 8 KiB L1-resident arrays, i7-12700H, best of 30 runs,
-O2:llvm-mca (llvm 22) on clang 22 output, cycles per vector including load and store:
Instructions per vector: AVX2 32 -> 12, SSSE3 36 -> 13.
Other ops built on u8
Shlbenefit on targets without AVX3_DL: u8Rol/Rorwith a vector count,MaskedShl/MaskedShlOr, andBitShuffle.Testing
TestVariableLeftShifts8inshift_test.cc: every 8-bit value with every shift count 0..7, in all lane positions, foruint8_tandint8_ton all partial vector sizes.shift_testpasses with clang 22 and gcc 16 on AVX2, SSE4, SSSE3, SSE2 and EMU128, and under Intel SDE (-spr) on AVX3_SPR, AVX3_DL and AVX3.rotate_testandbit_permute_test(which use u8ShlviaRolandBitShuffle) pass with clang 22.