Visitar URL original
x86: faster per-lane variable Shl for 8-bit lanes on AVX2/SSSE3/SSE4 by mvanslobbe · Pull Request #3523 · google/highway · GitHub
Skip to content

x86: faster per-lane variable Shl for 8-bit lanes on AVX2/SSSE3/SSE4 - #3523

Open
mvanslobbe wants to merge 5 commits into
google:masterfrom
mvanslobbe:perf/x86-u8-shl
Open

mvanslobbe wants to merge 5 commits into
google:masterfrom
mvanslobbe:perf/x86-u8-shl

Conversation

@mvanslobbe

Copy link
Copy Markdown

Below AVX3_DL, Shl(Vec<uint8_t>, Vec<uint8_t>) widens to 32-bit lanes for vpsllvd (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 one TableLookupBytes, then uses two 16-bit multiplies. The low byte of a16 * pow2 depends 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 of a16 is isolated and multiplied by pow2 >> 8, so the high byte of that product is the result. OrAnd merges 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, pshufb would 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:

Compiler AVX2 before AVX2 after SSSE3 before SSSE3 after
clang 14 114.6 20.4 5.62x 128.0 40.4 3.17x
clang 18 114.7 21.5 5.33x 145.4 46.2 3.15x
clang 22 130.6 23.3 5.61x 149.3 47.2 3.17x
gcc 13 139.4 27.2 5.12x 158.2 47.7 3.31x
gcc 15 143.3 25.1 5.70x 192.1 51.1 3.76x
gcc 16 143.3 24.4 5.87x 190.2 49.7 3.83x
gcc 17 (trunk 20261008) 143.2 24.4 5.87x 181.3 48.3 3.75x

llvm-mca (llvm 22) on clang 22 output, cycles per vector including load and store:

CPU AVX2 before AVX2 after SSSE3 before SSSE3 after
Haswell 26.1 4.1 10.1 5.1
Skylake 20.1 3.1 10.1 3.1
Alder Lake 17.1 2.9 9.8 2.9
Zen 4 11.1 3.6

Instructions per vector: AVX2 32 -> 12, SSSE3 36 -> 13.

Other ops built on u8 Shl benefit on targets without AVX3_DL: u8 Rol/Ror with a vector count, MaskedShl/MaskedShlOr, and BitShuffle.

Testing

  • New TestVariableLeftShifts8 in shift_test.cc: every 8-bit value with every shift count 0..7, in all lane positions, for uint8_t and int8_t on all partial vector sizes.
  • shift_test passes 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_test and bit_permute_test (which use u8 Shl via Rol and BitShuffle) pass with clang 22.
  • Standalone check of all values against counts 0..255 (expecting 0 for counts >= 8) on AVX2 and SSSE3 with the seven compilers above.

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 jan-wassenberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread hwy/ops/x86_128-inl.h
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)>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread hwy/ops/x86_128-inl.h Outdated
// 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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using a single 0x00FF constant for OrAnd and using AndNot for the inverse?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 248372b: one 0x00FF constant, shared by AndNot for the odd bytes and by OrAnd.

Comment thread hwy/tests/shift_test.cc Outdated
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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to decorrelate odd and even entries - maybe Xor odds with random bits (Random64).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread hwy/ops/x86_128-inl.h Outdated
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))));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip the Min() - previously, we were not clamping either.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@mvanslobbe

Copy link
Copy Markdown
Author

Thanks for the review! Good idea on Shr. I'll send that as a follow-up PR.

The bazel failure looks unrelated: hwy/os_rng.cc fails with "absolute path inclusion(s) found in rule '//:os_rng'".

@jan-wassenberg jan-wassenberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating, LGTM :)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants