3 versions, all work in 32 or 64-bit mode:
x86 machine code (clmul + AVX-512), 17 bytes. (Or 16B with AMD XOP instead of AVX-512)
x86 machine code (clmul + AVX1 32->64-bit, output pair in same order as input pair), 18 bytes
x86 scalar machine code (with BMI2 pdep
), 22 bytes
x86 machine code (clmul + AVX-512), 17 bytes
Takes 2 integers in the low two 16-bit elements of XMM0, returns 2 integers in 32-bit elements #1 and #0 of XMM0 (in that order, opposite of the input order). Swapping them would take another 4-byte instruction, shufps xmm0, 0b00_01
(or a left shift of the original and a different rotate count), but if we're willing to stretch the bounds of calling-convention plausibility, this is fine. If not, use the 18 byte version below.
NASM listing: address, hexdump of machine code, source
;;; 16-bit inputs packed in XMM0, 32-bit outputs packed in XMM0 (opposite order)
bit_interleave_bothways_avx512:
00 C4E37944C000 vpclmulqdq xmm0, xmm0, 0 ; space out each bit
; movq rax, xmm0
; rol rax, 33 ; non-AVX512 emulation for the low qword
; movq xmm1, rax
06 62F1F50872C821 vprolq xmm1, xmm0, 33 ; <<=1 and swap 32-bit halves
0D 0F56C1 orps xmm0, xmm1 ; combine
10 C3 ret
Key building block: Carryless multiply of a number by itself spaces out the bits, interleaving with zeros. x86 has that as pclmulqdq
, single-uop on modern Intel, e.g. Skylake (and 4 uops on AMD Zen1/2/3). Same size for VEX or legacy-SSE encoding, 6 bytes. With an immediate 0
, reads the low qword of each input, writes a 128-bit output to the full dqword XMM register.
AVX-512 has SIMD rotates, but unfortunately earlier Intel doesn't. (TODO: AMD's XOP instruction set has vprotq
with a 6-byte encoding according to NASM, 1 shorter than AVX-512 EVEX. AFAIK equivalent behaviour with an immediate count of 33. Would only run on AMD Bulldozer-family, which I don't have, so would be inconvenient to test but would still satisfy the rules of working on at least one implementation of a language.)
Combining the swap to line up outputs with each other along with a shift by 1 bit can be done with one rotate, but only with an EVEX encoding (4-byte prefix + opcode + modRM + imm8 = 7-byte encoding.) And only within a 64-bit qword, which means limiting the output size to two 32-bit integers, which means limiting the input to 16-bit integers.
Legacy SSE1 encodings are the smallest, only the escape byte no mandatory prefixes. We didn't dirty any YMM upper halves, so there's no SSE/AVX transition penalty here from mixing VEX/EVEX with legacy SSE. vorps xmm0, xmm0, xmm1
or por xmm0, xmm1
would each be 4 bytes.
x86 machine code (clmul 32->64-bit), 18 bytes
Requires only AVX1 + CLMUL CPU features, e.g. Sandybridge.
As above with clmul
, but using separate left-shift and swapping halves of the output to line them up for OR. This way we can handle 32-bit inputs (and the corresponding 64-bit outputs), and output in the same order as inputs.
Includes comments that trace the 4,9 test-case through it, confirmed from actual single-stepping in GDB. Would have output in hex, but the test cases strangely use decimal despite this being about bit-manipulation.
bit_interleave_bothways_avx512_32it:
; xmm0.v4_int32 = { 4, 9, 0, 0}
30 C4E37944C000 vpclmulqdq xmm0, xmm0, 0 ; xmm0.v2_int64 = { 16, 65 } value after clmul, from GDB
36 C5F9D4C8 vpaddq xmm1, xmm0, xmm0 ; xmm1 = { 32, 130 } ; VEX allows copy + left-shift with same size as paddw
3A 0FC6C04E shufps xmm0, xmm0, 0b01_00_11_10 ; xmm0 = { 65, 16 }
3E 0F56C1 orps xmm0, xmm1 ; xmm0 = { 97, 146 } ; in-place swap saves a byte vs. pshufd
41 C3 ret
Two 4-byte instructions (vpaddq
and shufps
) are only 1 byte larger than a single 7-byte EVEX vprolq
.
x86 scalar machine code (with BMI2), 22 bytes
bit_interleave_bothways:
00 BA55555555 mov edx, 0x55555555
05 C4E243F5FA pdep edi, edi, edx
0A C4E24BF5F2 pdep esi, esi, edx
0F 8D047E lea eax, [rsi + rdi*2] ; low bit = low b of arg1 = first out
12 8D1477 lea edx, [rdi + rsi*2] ; low bit = low b of arg2 = second out
15 C3 ret
Callable with the x86-64 System V calling convention (args in EDI, ESI),
return values in EAX (first), EDX (second).
The output is only 32 bits wide. This limit is more justifiable in 32-bit mode, where the same machine code does the same thing, disassembling as lea eax, [esi + edi*2]
and so on. (Using 64-bit operand-size would cost an extra REX.W byte on each PDEP and LEA, and need a 10-byte mov rdx, 0x5555555555555555
instead of 5-byte mov reg, imm32
, so an extra 9 bytes total). The same machine code would not work in 16-bit mode; scaled index addressing modes would need an address-size prefix. VEX prefixes (for pdep
) only decode in 16-bit protected mode, not real.
The key building block is pdep
, parallel bit-deposit, which takes low source bits and puts them at places where the control mask was non-zero. With 0b...010101
, this unpacks each input by interleaving it with zeros.
Doing (x << 1) + y
and vice versa with LEA merges the unpacked bits into a single interleaved integer. +
instead of the more idiomatic |
allows using the shift-and-add LEA instruction.
Try it online! with a brief test harness. (Only uses the first return value as exit status; I tested by single-stepping in a debugger. Not updated for the later CLMUL versions.)