AVX2什么是基于掩码打包左边最有效的方法?

如果你有一个input数组和一个输出数组,但你只想写出那些通过一定条件的元素,那么在AVX2中这样做最有效的方法是什么?

我在SSE看到它是这样做的:(从: https : //deplinenoise.files.wordpress.com/2015/03/gdc2015_afredriksson_simd.pdf )

__m128i LeftPack_SSSE3(__m128 mask, __m128 val) { // Move 4 sign bits of mask to 4-bit integer value. int mask = _mm_movemask_ps(mask); // Select shuffle control data __m128i shuf_ctrl = _mm_load_si128(&shufmasks[mask]); // Permute to move valid values to front of SIMD register __m128i packed = _mm_shuffle_epi8(_mm_castps_si128(val), shuf_ctrl); return packed; } 

这对于4宽的SSE来说似乎很好,因此只需要16个入口LUT,但对于8宽的AVX,LUT变得相当大(256个入口,每个32个字节或8K)。

我很惊讶AVX似乎没有简化这个过程的指令,比如带有包装的蒙面店。

我想通过一些混洗来计算左侧设置的符号位数,您可以生成必要的置换表,然后调用_mm256_permutevar8x32_ps。 但是这也是我认为的一些说明。

有没有人知道有什么把戏与AVX2做到这一点? 或者什么是最有效的方法?

下面是上述文件中左侧包装问题的说明:

Left.Packing.Problem

谢谢

AVX2 + BMI2。 看到我的AVX512的其他答案。 (更新:在64位版本中保存了一个pdep 。)

我们可以使用AVX2 vpermps_mm256_permutevar8x32_ps ) (或者等价的整数, vpermd )来进行通道variables的混洗。

我们可以即时产生掩码 ,因为BMI2 pext (并行位提取)为我们提供了所需操作的按位版本。


对于具有32位或更宽元素的整数向量 :1) _mm256_movemask_ps(_mm256_castsi256_ps(compare_mask))
或2)使用_mm256_movemask_epi8 ,然后将第一个PDEP常量从0x0101010101010101更改为0x0F0F0F0F0F0F0F0F,以分散4个连续位的块。 将乘以0xFFU更改为expanded_mask |= expanded_mask<<4;expanded_mask *= 0x11; (未testing)。 无论采用哪种方法,都应使用VPERMD而不是VPERMPS的混洗掩码。

对于64位整型或double元素,一切仍然正常工作 ; 比较掩码恰好总是具有相同的32位元素对,所以最终的混洗将每个64位元素的两半放在正确的位置。 (所以你仍然使用VPERMPS或VPERMD,因为VPERMPD和VPERMQ只能用于立即控制操作数。)


algorithm:

从一个常量的3位索引开始,每个位置都有自己的索引。 即[ 7 6 5 4 3 2 1 0 ]其中每个元素是3位宽。 0b111'110'101'...'010'001'000

使用pext将我们想要的索引提取到整数寄存器底部的连续序列中。 例如,如果我们想索引0和2,我们的控制面膜的pext应该是0b000'...'111'000'111pext将抓取与select器中的1位pext010000索引组。 所选的组被压缩到输出的低位,所以输出将是0b000'...'010'000 。 (即[ ... 2 0 ]

请参阅注释代码,了解如何从input向量掩码中为pext生成0b111000111input。

现在,我们和压缩查询表(LUT)在同一条船上:打包最多8个指数。

当你把所有的东西放在一起时,总共有三个pext / pdep 。 我从我想要的东西后退了,所以也可以在这个方向上理解它。 (即从洗牌开始,从那里开始工作)

如果我们使用每个字节一个索引而不是打包的3位组,我们可以简化拆包 。 由于我们有8个索引,所以只能使用64位的代码。

在Godbolt编译器资源pipe理器上看到这个和32位版本 。 我使用了#ifdef所以用-m64-m32编译。 海湾合作委员会浪费一些指示,但铿锵使非常好的代码。

 #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 

所以,根据Agner Fog的数字 ,这是6个uops (不包括常量,或者内联时消失的零扩展mov)。 在Intel Haswell上,它的延迟是16c(vmovq为1,每个pdep / imul / pext / vpmovzx / vpermps为3)。 没有指令级的并行性。 然而,在一个循环中,这不是一个循环运行的依赖的一部分(就像我在Godbolt链路中所包含的那个循环一样),瓶颈有希望只是吞吐量,同时在一次飞行中保持多次迭代。

这可能会pipe理每3个周期一个吞吐量,pdep / pext / imul的port1瓶颈。 当然,对于加载/存储和循环开销(包括compare,movmsk和popcnt),总的uop吞吐量很容易成为问题。 (例如,我的godbolt链接中的filter循环是14个uip,带有clang,使用-fno-unroll-loops可以更容易阅读,每个4c可以保持一个迭代,跟上前端,如果我们幸运的话,但是我觉得clang没有考虑到popcnt对它的输出的错误依赖,所以它会在compress256函数的延迟时间的popcnt瓶颈。)

gcc用0xFF乘以多条指令,使用左移8和一个sub指令。 这需要额外的mov指令,但最终的结果是延迟为2的乘法(Haswell在寄存器重命名阶段处理mov ,零延迟)。


由于所有支持AVX2的硬件也支持BMI2,因此在没有BMI2的情况下为AVX2提供版本可能没有意义。

如果你需要在一个非常长的循环中完成这个工作,那么如果最初的caching未命中经过足够的迭代,只需拆开LUT条目的开销就可以了。 您仍然需要movmskps ,所以您可以popup掩码并将其用作LUT索引,但可以保存pdep / imul / pexp。

你可以用我使用的相同的整数序列来解压缩LUT条目,但是当LUT条目在内存中开始并且不需要首先进入整数寄存器时,@ Froglegs的set1() / vpsrlvd / vpand可能会更好。 (32位广播负载不需要Intel CPU上的ALU uop)。 然而,哈斯韦尔(Haswell)上的variables是3个variables(Skylake中只有1个variables)。

查看我的AVX2 + BMI2没有LUT的其他答案。

既然你提到了对AVX512的可扩展性的担忧,不要担心, AVX512F的指令就是这样的

VCOMPRESSPS – 将稀疏打包的单精度浮点值存储到密集内存中。 (也有两个版本,32或64位整数元素( vpcompressq ),但不是字节或字(16位))。 它就像BMI2 pdep / pext ,但对于vector而不是整数reg中的位。

目标可以是向量寄存器或内存操作数,而源是向量和掩码寄存器。 使用寄存器dest,它可以合并或置零高位。 使用内存目标,“只有连续的向量被写入到目标内存位置”。

为了弄清楚要在下一个向量中移动多less指针,请popup面罩。

假设你想过滤掉所有的值,但是数组中的值大于等于0:

 #include <stdint.h> #include <immintrin.h> size_t filter_non_negative(float *__restrict__ dst, const float *__restrict__ src, size_t len) { const float *endp = src+len; float *dst_start = dst; do { __m512 sv = _mm512_loadu_ps(src); __mmask16 keep = _mm512_cmp_ps_mask(sv, _mm512_setzero_ps(), _CMP_GE_OQ); // true for src >= 0.0, false for unordered and src < 0.0 _mm512_mask_compressstoreu_ps(dst, keep, sv); // clang is missing this intrinsic, which can't be emulated with a separate store src += 16; dst += _mm_popcnt_u64(keep); // popcnt_u64 instead of u32 helps gcc avoid a wasted movsx, but is potentially slower on some CPUs } while (src < endp); return dst - dst_start; } 

这个编译(用gcc4.9或更高版本)到( Godbolt编译器资源pipe理器 ):

  # Output from gcc6.1, with -O3 -march=haswell -mavx512f. Same with other gcc versions lea rcx, [rsi+rdx*4] # endp mov rax, rdi vpxord zmm1, zmm1, zmm1 # vpxor xmm1, xmm1,xmm1 would save a byte, using VEX instead of EVEX .L2: vmovups zmm0, ZMMWORD PTR [rsi] add rsi, 64 vcmpps k1, zmm0, zmm1, 29 # AVX512 compares have mask regs as a destination kmovw edx, k1 # There are some insns to add/or/and mask regs, but not popcnt movzx edx, dx # gcc is dumb and doesn't know that kmovw already zero-extends to fill the destination. vcompressps ZMMWORD PTR [rax]{k1}, zmm0 popcnt rdx, rdx ## movsx rdx, edx # with _popcnt_u32, gcc is dumb. No casting can get gcc to do anything but sign-extend. You'd expect (unsigned) would mov to zero-extend, but no. lea rax, [rax+rdx*4] # dst += ... cmp rcx, rsi ja .L2 sub rax, rdi sar rax, 2 # address math -> element count ret 

我想出了这个方法,它使用了一个压缩的LUT,它是768(+1填充)字节,而不是8K。 它需要广播单个标量值,然后在每个通道中移动一个不同的数量,然后屏蔽到较低的3位,从而提供0到7个LUT。

这里是intrinsics版本,以及构buildLUT的代码。

 //Generate Move mask via: _mm256_movemask_ps(_mm256_castsi256_ps(mask)); etc __m256i MoveMaskToIndices(int moveMask) { u8 *adr = g_pack_left_table_u8x3 + moveMask * 3; __m256i indices = _mm256_set1_epi32(*reinterpret_cast<u32*>(adr));//lower 24 bits has our LUT __m256i m = _mm256_sllv_epi32(indices, _mm256_setr_epi32(29, 26, 23, 20, 17, 14, 11, 8)); //now shift it right to get 3 bits at bottom __m256i shufmask = _mm256_srli_epi32(m, 29); return shufmask; } u32 get_nth_bits(int a) { u32 out = 0; int c = 0; for (int i = 0; i < 8; ++i) { auto set = (a >> i) & 1; if (set) { out |= (i << (c * 3)); c++; } } return out; } u8 g_pack_left_table_u8x3[256 * 3 + 1]; void BuildPackMask() { for (int i = 0; i < 256; ++i) { *reinterpret_cast<u32*>(&g_pack_left_table_u8x3[i * 3]) = get_nth_bits(i); } } 

这是由VS2015生成的程序集:

 lea eax, DWORD PTR [rcx+rcx*2] movsxd rcx, eax lea rax, OFFSET FLAT:?g_pack_left_table_u8x3@@3PAEA ; g_pack_left_table_u8x3 vpbroadcastd ymm0, DWORD PTR [rcx+rax] vpsllvd ymm0, ymm0, YMMWORD PTR __ymm@000000080000000b0000000e0000001100000014000000170000001a0000001d vpsrld ymm0, ymm0, 29 

如果有人感兴趣,这里是一个SSE2的解决scheme,它使用一个指令LUT,而不是一个数据LUT又名跳转表。 有了AVX,这将需要256个案例。

每次你在下面调用LeftPack_SSE2它基本上使用三个指令:jmp,shufps,jmp。 十六个案例中的五个不需要修改向量。

 static inline __m128 LeftPack_SSE2(__m128 val, int mask) { switch(mask) { case 0: case 1: return val; case 2: return _mm_shuffle_ps(val,val,0x01); case 3: return val; case 4: return _mm_shuffle_ps(val,val,0x02); case 5: return _mm_shuffle_ps(val,val,0x08); case 6: return _mm_shuffle_ps(val,val,0x09); case 7: return val; case 8: return _mm_shuffle_ps(val,val,0x03); case 9: return _mm_shuffle_ps(val,val,0x0c); case 10: return _mm_shuffle_ps(val,val,0x0d); case 11: return _mm_shuffle_ps(val,val,0x34); case 12: return _mm_shuffle_ps(val,val,0x0e); case 13: return _mm_shuffle_ps(val,val,0x38); case 14: return _mm_shuffle_ps(val,val,0x39); case 15: return val; } } __m128 foo(__m128 val, __m128 maskv) { int mask = _mm_movemask_ps(maskv); return LeftPack_SSE2(val, mask); }