movemask指令采用__m256i并返回一个int32,其中每个位(取决于输入向量元素类型的前4位,8位或所有32位)是相应向量元素的最高位。 / p>
我想做反过来:取32(其中只有4,8或32个最低有效位有意义),得到__m256i,其中每个int8,int32或int64大小的块的最高位被设置到原来的位。
基本上,我想从压缩的位掩码转到可被其他AVX2指令(例如maskstore,maskload,mask_gather)用作掩码的位掩码。
我无法快速找到执行此操作的说明,所以我在这里问。 如果没有一条具有该功能的指令,您是否可以想到一个聪明的黑客,只需很少的指令即可实现这一点?
我目前的方法是使用256元素查找表。 我想在一个没有其他事情发生的循环中使用这个操作来加速它。注意,我对长多指令序列或实现此操作的小循环不太感兴趣。
答案 0 :(得分:8)
AVX2或更早版本中没有单一指令。
vpbroadcastw
/ vpand
/ vpcmpeqw
如果您从内存中加载位图,将其直接加载到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
在英特尔可能更糟糕。
当整个位图适合每个元素时,广播它,带有选择器掩码的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微博,它在加载端口完成。 (b
和w
是加载+随机播放)。即使您的掩码被打包在一起(每个字节对应32或64位元素),vpbroadcastd
而不是vpbroadcastb
可能仍然更有效。在广播之后,x & mask == mask
检查并不关心每个元素的高字节中的垃圾。唯一的担心是缓存行/页面拆分。
变量混合和屏蔽加载/存储只关心掩码元素的符号位。
一旦你将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吞吐量一个。
请参阅x86代码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位。
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}}到处乱七八糟。)