AVX2 + BMI2。请参阅我对 AVX512 的其他答案。 (更新:保存了pdep
在 64 位版本中。)
我们可以用AVX2 vpermps (_mm256_permutevar8x32_ps)(或等价的整数,vpermd
)进行车道交叉可变洗牌。
我们可以动态生成蒙版,自 BMI2pext(并行位提取)为我们提供了所需操作的按位版本。
当心pdep
/pext
are veryZen 3 之前的 AMD CPU 上速度较慢,如 Ryzen Zen 1 和 Zen 2 上的 6 uops/18 周期延迟和吞吐量。此实现在 AMD CPU 上的性能将非常糟糕。对于 AMD,您可能最好使用 128 位向量pshufb
or vpermilps
LUT,或评论中讨论的一些 AVX2 可变移位建议。特别是如果您的掩码输入是矢量掩码(不是内存中已打包的位掩码)。
Zen2之前的AMD无论如何都只有128位向量执行单元,256位跨车道shuffle速度很慢。因此,128 位向量在 Zen 1 上对此非常有吸引力。但 Zen 2 有 256 位加载/存储和执行单元。 (而且微编码 pext/pdep 仍然很慢。)
对于具有 32 位或更宽元素的整数向量: 任一 1)_mm256_movemask_ps(_mm256_castsi256_ps(compare_mask))
.
或者2)使用_mm256_movemask_epi8
然后将第一个 PDEP 常量从 0x0101010101010101 更改为 0x0F0F0F0F0F0F0F0F 以分散 4 个连续位的块。将乘以0xFFU改为expanded_mask |= expanded_mask<<4;
or expanded_mask *= 0x11;
(未测试)。无论哪种方式,请使用带有 VPERMD 的随机播放掩码,而不是 VPERMPS。
对于 64 位整数或double
元素,一切仍然正常;比较掩码恰好总是具有相同的 32 位元素对,因此生成的洗牌将每个 64 位元素的两半放在正确的位置。 (因此您仍然使用 VPERMPS 或 VPERMD,因为 VPERMPD 和 VPERMQ 仅适用于立即控制操作数。)
对于 16 位元素,您也许可以使用 128 位向量来适应这一点。
对于 8 位元素,请参见用于左包装字节元素的高效 sse shuffle mask 生成对于不同的技巧,将结果存储在多个可能重叠的块中。
算法:
从压缩的 3 位索引常量开始,每个位置都有自己的索引。 IE。[ 7 6 5 4 3 2 1 0 ]
其中每个元素都是 3 位宽。0b111'110'101'...'010'001'000
.
Use pext
将我们想要的索引提取到整数寄存器底部的连续序列中。例如如果我们想要索引 0 和 2,我们的控制掩码pext
应该0b000'...'111'000'111
. pext
将抓住010
and 000
与选择器中的 1 位对齐的索引组。所选组被打包到输出的低位中,因此输出将是0b000'...'010'000
。 (IE。[ ... 2 0 ]
)
查看注释代码了解如何生成0b111000111
输入为pext
来自输入向量掩码。
现在我们与压缩 LUT 处于同一条船上:解压最多 8 个压缩索引。
当你把所有的部分放在一起时,总共有三个pext
/pdep
s。我从我想要的方向逆向工作,所以从那个方向理解它可能也是最容易的。 (即从随机播放线开始,然后从那里向后进行。)
如果我们使用每个字节一个索引而不是打包的 3 位组,我们可以简化解包。由于我们有 8 个索引,因此只有 64 位代码才可能实现。
See 这个版本和 Godbolt Compiler Explorer 上的纯 32 位版本。我用了#ifdef
s 因此它可以最佳地编译-m64
or -m32
。 gcc 浪费了一些指令,但 clang 编写了非常好的代码。
#include <stdint.h>
#include <immintrin.h>
// Uses 64bit pdep / pext to save a step in unpacking.
__m256 compress256(__m256 src, unsigned int mask /* from movmskps */)
{
uint64_t expanded_mask = _pdep_u64(mask, 0x0101010101010101); // unpack each bit to a byte
expanded_mask *= 0xFF; // mask |= mask<<1 | mask<<2 | ... | mask<<7;
// ABC... -> AAAAAAAABBBBBBBBCCCCCCCC...: replicate each bit to fill its byte
const uint64_t identity_indices = 0x0706050403020100; // the identity shuffle for vpermps, packed to one index per byte
uint64_t wanted_indices = _pext_u64(identity_indices, expanded_mask);
__m128i bytevec = _mm_cvtsi64_si128(wanted_indices);
__m256i shufmask = _mm256_cvtepu8_epi32(bytevec);
return _mm256_permutevar8x32_ps(src, shufmask);
}
这会编译为不从内存加载的代码,只有立即常量。 (请参阅该版本和 32 位版本的 godbolt 链接)。
# clang 3.7.1 -std=gnu++14 -O3 -march=haswell
mov eax, edi # just to zero extend: goes away when inlining
movabs rcx, 72340172838076673 # The constants are hoisted after inlining into a loop
pdep rax, rax, rcx # ABC -> 0000000A0000000B....
imul rax, rax, 255 # 0000000A0000000B.. -> AAAAAAAABBBBBBBB..
movabs rcx, 506097522914230528
pext rax, rcx, rax
vmovq xmm1, rax
vpmovzxbd ymm1, xmm1 # 3c latency since this is lane-crossing
vpermps ymm0, ymm1, ymm0
ret
(后来clang像GCC一样编译,用mov/shl/sub代替imul,见下文。)
所以,根据阿格纳·福格的号码 and https://uops.info/,这是 6 uop(不计算常量,或内联时消失的零扩展 mov)。在 Intel Haswell 上,延迟为 16c(vmovq 为 1,每个 pdep/imul/pext/vpmovzx/vpermps 为 3)。不存在指令级并行性。不过,在一个循环中,这不是循环携带依赖项的一部分(就像我在 Godbolt 链接中包含的那样),瓶颈希望只是吞吐量,同时保持多次迭代。
这可能可以管理每 4 个周期一个的吞吐量,在循环中的 pdep/pext/imul 加上 popcnt 的端口 1 上出现瓶颈。当然,由于加载/存储和其他循环开销(包括比较和 movmsk),总 uop 吞吐量也很容易成为一个问题。
例如我的 Godbolt 链接中的过滤器循环是 14 uops,带有 clang,-fno-unroll-loops
使其更易于阅读。如果我们幸运的话,它可能会维持每 4c 一次迭代,跟上前端的步伐。
clang 6 及更早版本创建了一个循环携带的依赖项popcnt对其输出的错误依赖,所以它的瓶颈是 3/5 的延迟compress256
功能。 clang 7.0 及更高版本使用异或归零来打破错误的依赖关系(而不是仅仅使用popcnt edx,edx
或者像 GCC 那样的东西:/)。
gcc(以及后来的 clang)使用多条指令进行乘以 0xFF,使用左移 8 和sub
, 代替imul
255。这总共需要 3 个 uops,而前端则需要 1 个,但延迟仅为 2 个周期,比 3 个周期低。(Haswell 处理mov
在寄存器重命名阶段,零延迟。)最重要的是,imul
只能在端口 1 上运行,与 pdep/pext/popcnt 竞争,因此最好避免该瓶颈。
由于所有支持 AVX2 的硬件也支持 BMI2,因此提供没有 BMI2 的 AVX2 版本可能没有意义。
如果您需要在很长的循环中执行此操作,如果初始缓存未命中在足够的迭代中分摊,并且仅解压 LUT 条目的开销较低,那么 LUT 可能是值得的。你还需要movmskps
,因此您可以 popcnt 掩码并将其用作 LUT 索引,但您可以保存 pdep/imul/pext。
您可以使用我使用的相同整数序列来解压 LUT 条目,但是 @Froglegs 的set1()
/ vpsrlvd
/ vpand
当 LUT 条目在内存中开始并且不需要首先进入整数寄存器时,可能会更好。 (32 位广播负载在 Intel CPU 上不需要 ALU uop)。然而,可变移位在 Haswell 上为 3 uops(但在 Skylake 上仅为 1)。