是否有针对intel avx2中的movemask指令的反向指令?

时间:2016-04-07 23:01:00

标签: x86 intrinsics avx avx2 icc

movemask指令采用__m256i并返回一个int32,其中每个位(取决于输入向量元素类型的前4位,8位或所有32位)是相应向量元素的最高位。 / p>

我想做反过来:取32(其中只有4,8或32个最低有效位有意义),得到__m256i,其中每个int8,int32或int64大小的块的最高位被设置到原来的位。

基本上,我想从压缩的位掩码转到可被其他AVX2指令(例如maskstore,maskload,mask_gather)用作掩码的位掩码。

我无法快速找到执行此操作的说明,所以我在这里问。 如果没有一条具有该功能的指令,您是否可以想到一个聪明的黑客,只需很少的指令即可实现这一点?

我目前的方法是使用256元素查找表。 我想在一个没有其他事情发生的循环中使用这个操作来加速它。注意,我对长多指令序列或实现此操作的小循环不太感兴趣。

1 个答案:

答案 0 :(得分:8)

AVX2或更早版本中没有单一指令。

如果您从内存中加载位图,将其直接加载到ALU策略的向量寄存器中应该可以正常工作。

如果您将位图作为计算结果,那么它将位于整数寄存器中,您可以轻松地将其用作LUT索引,因此,如果您的目标是64-,那么这是一个不错的选择。位元素。否则可能仍然是ALU为32位元素或更小,而不是一个巨大的LUT或做多个块。

在从整数位掩码到矢量掩码的廉价转换成为可能之前,我们必须等待AVX-512的掩码寄存器。 (使用kmovw k1, r/m16,哪些编译器隐式生成int => __mmask16)。有一个AVX512 insn可以设置面具中的矢量(VPMOVM2D zmm1, k1 _mm512_movm_epi8/16/32/64 ,其他版本适用于不同的元素尺寸),但您通常不会这样做。因为过去使用掩码向量的所有内容现在都使用掩码寄存器,所以需要它。也许你想要计算符合某种比较条件的元素? (您使用pcmpeqd / psubd来生成和累积0或-1元素的向量)。但是掩码结果上的标量popcnt将是更好的选择。

但请注意vpmovm2d要求掩码位于AVX512 k0..7掩码寄存器中。获取它将需要额外的指令,除非它来自矢量比较结果,并且进入掩码寄存器的指令需要在Intel Skylake-X和类似CPU上使用端口5的uop,因此这可能是瓶颈(特别是如果你做任何改组) )。特别是如果它从内存开始(加载位图)并且你只需要每个元素的高位,即使256位和512位AVX512指令是广播加载+变量移位,你可能仍然会更好。可用。

对于64位元素,掩码只有4位,因此查找表是合理的。您可以通过加载VPMOVSXBQ ymm1, xmm2/m32. (_mm256_cvtepi8_epi64)来压缩LUT。这使得LUT大小为(1 <&lt; 4)= 16 * 4字节= 64B = 1个高速缓存行。不幸的是,pmovsx is inconvenient to use as a narrow load with intrinsics

特别是如果你已经在整数寄存器(而不是内存)中使用了位图,那么vpmovsxbq LUT在64位元素的内部循环中应该是优秀的。或者,如果指令吞吐量或随机吞吐量是瓶颈,请使用未压缩的LUT。这可以让你(或编译器)使用掩码向量作为其他东西的内存操作数,而不需要单独的指令来加载它。

32位元素的LUT:可能不是最优的但是你可以这样做

对于32位元素,8位掩码为您提供256个可能的向量,每个长度为8个元素。 256 * 8B = 2048字节,即使对于压缩版本(使用vpmovsxbd ymm, m64加载),这也是一个相当大的缓存占用空间。

要解决此问题,您可以将LUT拆分为4位块。将大约8位整数分成两个4位整数(mov/and/shr)需要大约3个整数指令。然后使用128b向量的未压缩LUT(对于32位元素大小),vmovdqa为低半,vinserti128为高半。你仍然可以压缩LUT,但我不推荐它,因为你需要vmovd / vpinsrd / vpmovsxbd,这是2次洗牌(所以你可能是瓶颈关于uop吞吐量)。

或2x vpmovsxbd xmm, [lut + rsi*4] + vinserti128在英特尔可能更糟糕。

ALU替代方案:适用于16/32/64位元素

当整个位图适合每个元素时,广播它,带有选择器掩码的AND,以及针对相同常量的VPCMPEQ(可以在循环中多次使用它时保留在寄存器中)。

vpbroadcastd  ymm0,  dword [mask]
vpand         ymm0, ymm0,  [vec of 1<<0, 1<<1, 1<<2, 1<<3, ...]
vpcmpeqd      ymm0, ymm0,  [same constant]
      ; ymm0 =  (mask & bit) == bit
      ; where bit = 1<<element_number

(掩码可能来自带vmovd + vpbroadcastd的整数寄存器,但广播负载

对于8位元素,您需要vpshufb vpbroadcastd结果来获得每个字节的相关位。见How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)?。但是对于16位和更宽的元素,元素的数量是&lt; =元素宽度,因此广播负载是免费的。 (16位广播负载确实需要一个微融合ALU shuffle uop,不像32和64位广播负载完全在负载端口处理。)

vpbroadcastd/q甚至不会花费任何ALU微博,它在加载端口完成。 (bw是加载+随机播放)。即使您的掩码被打包在一起(每个字节对应32或64位元素),vpbroadcastd而不是vpbroadcastb可能仍然更有效。在广播之后,x & mask == mask检查并不关心每个元素的高字节中的垃圾。唯一的担心是缓存行/页面拆分。

如果您只需要符号位

,可变班次(Skylake便宜)

变量混合和屏蔽加载/存储只关心掩码元素的符号位。

一旦你将8位掩码广播到dword元素,这只是1 uop(在Skylake上)。

vpbroadcastd  ymm0, dword [mask]

vpsllvd       ymm0, ymm0, [vec of 24, 25, 26, 27, 28, 29, 30, 31]  ; high bit of each element = corresponding bit of the mask

;vpsrad        ymm0, ymm0, 31                          ; broadcast the sign bit of each element to the whole element
;vpsllvd + vpsrad has no advantage over vpand / vpcmpeqb, so don't use this if you need all the bits set.

vpbroadcastd和内存负载一样便宜(在Intel CPU和Ryzen上根本没有ALU uop)。 (较窄的广播,如vpbroadcastb y,mem对英特尔采取ALU改组,但可能不在Ryzen上。)

Haswell / Broadwell(3 uops,执行端口有限)的变速稍微贵一些,但与Skylake的即时计数一样便宜! (在端口0或1上1个uop。)在Ryzen上,它们也只有2个uop(任何256b操作的最小值),但是具有3c延迟和每4c吞吐量一个。

请参阅代码wiki了解perf信息,尤其是Agner Fog's insn tables

对于64位元素,请注意算术右移仅适用于16位和32位元素大小。如果您希望将整个元素设置为全零 - 全部为4位,则使用不同的策略 - &gt; 64位元素。

使用内在函数:

__m256i bitmap2vecmask(int m) {
    const __m256i vshift_count = _mm256_set_epi32(24, 25, 26, 27, 28, 29, 30, 31);
    __m256i bcast = _mm256_set1_epi32(m);
    __m256i shifted = _mm256_sllv_epi32(bcast, vshift_count);  // high bit of each element = corresponding bit of the mask
    return shifted;

    // use _mm256_and and _mm256_cmpeq if you need all bits set.
    //return _mm256_srai_epi32(shifted, 31);             // broadcast the sign bit to the whole element
}

在循环内部,LUT可能值得缓存占用空间,具体取决于循环中的指令混合。特别是对于64位元素大小,它的缓存占用空间不大,但甚至可能是32位。

另一个选择,而不是变量移位,是使用BMI2将每个位解包为一个字节,并在高位中使用该掩码元素,然后vpmovsx

; 8bit mask bitmap in eax, constant in rdi

pdep      rax, rax, rdi   ; rdi = 0b1000000010000000... repeating
vmovq     xmm0, rax
vpmovsxbd ymm0, xmm0      ; each element = 0xffffff80 or 0

; optional
;vpsrad    ymm0, ymm0, 8   ; arithmetic shift to get -1 or 0

如果你已经在整数寄存器中有掩码(无论如何你必须分别vmovq / vpbroadcastd),那么即使在变量计数转换的Skylake上,这种方式可能更好很便宜。

如果你的掩码从内存开始,另一个ALU方法(vpbroadcastd直接进入矢量)可能更好,因为广播负载非常便宜。

请注意,pdep是Ryzen的6个依赖uops(18c延迟,18c吞吐量),所以即使你的掩码以整数注册开始,这个方法在Ryzen上也很糟糕。

(未来的读者,可以自由编辑这个内在版本。编写asm更容易,因为它的输入更少,并且asm助记符更容易阅读(没有愚蠢{{ 1}}到处乱七八糟。)