Я хочу расширить некоторые u8 до u64, за исключением того, что вместо расширения нуля или знака, которые имеют прямую поддержку, я хочу «расширение копирования». Как лучше всего это сделать (на процессоре Intel с avx512)? Пример кода написан на Rust, но основной язык не представляет интереса.
#![feature(portable_simd)]
use std::simd::*;
// Expands out each input byte 8 times
pub fn batch_splat_scalar(x: [u8; 16]) -> [u64; 16] {
let mut ret = [0; 16];
for i in 0..16 {
ret[i] =
u64::from_le_bytes([x[i], x[i], x[i], x[i], x[i], x[i], x[i], x[i]]);
}
ret
}
pub fn batch_splat_simd(x: u8x16) -> u64x16 {
Simd::from_array(batch_splat_scalar(x.to_array()))
}
который компилируется примерно так с помощью avx512
vpmovzxbq zmm0, qword ptr [rsi]
vpbroadcastq zmm1, qword ptr [rip + .LCPI0_0]
mov rax, rdi
vpmuludq zmm2, zmm0, zmm1
vpbroadcastq zmm3, qword ptr [rip + .LCPI0_1]
vpmuludq zmm0, zmm0, zmm3
vpsllq zmm0, zmm0, 32
vporq zmm0, zmm2, zmm0
vmovdqu64 zmmword ptr [rdi], zmm0
vpmovzxbq zmm0, qword ptr [rsi + 8]
vpmuludq zmm1, zmm0, zmm1
vpmuludq zmm0, zmm0, zmm3
vpsllq zmm0, zmm0, 32
vporq zmm0, zmm1, zmm0
vmovdqu64 zmmword ptr [rdi + 64], zmm0
vzeroupper
ret

Значит, каждый элемент результата u64 содержит 8 копий соответствующего ввода u8? Я думаю, что лучше всего в asm vpermb использовать AVX-512VBMI (Ice Lake). При правильном векторе управления вы можете заставить каждый байт ZMM захватывать нужный вам байт из младших 16 байтов другого ZMM (т. е. XMM).
В противном случае транслируйте и vpshufb zmm. (https://www.felixcloutier.com/x86/pshufb)
Одна широковещательная загрузка размером от 128 до 512 бит может обеспечить два тасования с разными векторами управления. Или две vpbroadcastq 64-битные широковещательные загрузки могут передавать две vpshufb с одним и тем же вектором управления.
По крайней мере, на Intel широковещательная загрузка — это просто загрузка, а не ALU. (https://uops.info/). Поэтому, если вы все равно загружаете данные из памяти, выполните одну широковещательную загрузку и используйте 2x vpshufb вместо 2x vpermb, поскольку это дешевле (меньшая задержка, но по-прежнему только один порт выполнения).
Я не знаком с std::simd в Rust, но ассемблерный код, который он генерирует, очень плох, поскольку вместо инструкций перемешивания используются множественные битхаки (вероятно, с константами вроде 0x0101010101010101).
Ассе, который вам нужен, это что-то вроде
VBROADCASTI32X4 zmm0, [rsi] # the mem operand is 128-bit, an xmmword
vpshufb zmm1, zmm0, [.LC0] # or with the vector constants in regs if reused
vpshufb zmm0, zmm0, [.LC1]
Первая векторная константа — 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1 и т. д. Вторая — 8,8,8,8,8. ,8,8,8, 9,9,9,9,9,9,9,9 и т. д. Индексация происходит внутри каждой 128-битной полосы, поэтому мы использовали широковещательную загрузку.
Если бы мы использовали vpermb, мы могли бы использовать простой vmovdqu xmm0, [rsi], который экономит пару байт размера машинного кода, но при перетасовке будет более высокая задержка (но все равно та же пропускная способность, в том числе, по-видимому, в Zen 4). сложнее для выполнения вне очереди, что снижает общую пропускную способность.
Если ваши данные изначально уже находились в нижней части векторной регистрации, вы бы предпочли vpermb перетасовке ALU для трансляции или vpmovzx ее.
Я надеялся, что VPMULTISHIFTQB с 64-битным операндом источника широковещательной памяти будет еще лучше, но, очевидно, на процессорах Intel он не может микро-сплавиться в один цикл загрузки+перетасовки. Таким образом, использование его дважды не лучше, чем 2x vpbroadcastq загрузки плюс 2x vpshufb, за исключением небольшой экономии в размере машинного кода и различной упаковки в кэш uop, что может быть хуже или лучше. uops.info измерил, что vpmultishiftqb составляет 2 мопса для интерфейса на Ice Lake и Alder Lake (Sapphire Rapids). Это может быть победой над Zen 4, где он объединяется в единый моп.