如题所述,如果一个256位SIMD寄存器是:
0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
如何高效地获取第一个非零元素的索引(即第一个1的索引2)?最直接的方法是逐个存储到内存并检查,但这可能会代价太大。是否有什么巧妙的想法可以解决这个问题?
0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
如何高效地获取第一个非零元素的索引(即第一个1的索引2)?最直接的方法是逐个存储到内存并检查,但这可能会代价太大。是否有什么巧妙的想法可以解决这个问题?
movmskps
或pd
指令,每个dword或qword得到1个位而不是每个byte,如果这样做可以使您的位扫描 -> 索引计算更加高效,例如如果您想要一个元素偏移而不是字节偏移。)~
运算符,汇编中使用NOT指令),得到位图中非零元素的值为1。~bitmask
时,这不是一个问题——高16个零位变成了1。(使用vpmovmskb ymm
填充一个完整的uint32_t
的AVX2版本可能需要使用~(uint64_t)bitmask
,或者只使用tzcnt
,因为AVX2 CPU也具有BMI1。)int first_nonzero_byte(__m128i v){
//__m128i v = _mm_loadu_si128((const __m128i*)p); // for a pointer arg
__m128i vcmp = _mm_cmpeq_epi8(v, _mm_setzero_si128());
unsigned bitmask = _mm_movemask_epi8(vcmp);
#ifdef __GNUC__
return __builtin_ctz(~bitmask);
#else
return _tzcnt_u32( ~bitmask );
#endif
// returns 16 if v was all zero so ~bitmask is 0xFFFF0000
}
在https://godbolt.org/z/Y8vYbsW69上编译为
# GCC11.2 -O3 -msse4.1
movdqa xmm1, xmm0 # missed optimization, should zero XMM1 instead
pxor xmm0, xmm0
pcmpeqb xmm0, xmm1
pmovmskb eax, xmm0
not eax
rep bsf eax, eax # tzcnt on new CPUs, BSF on old
ret
-march=haswell
或其他编译选项,_tzcnt_u32
将无法编译,我们使用__builtin_ctz
。正如我所说,~bitmask
保证是非零的。tzcnt
被编码为rep bsf
;旧的CPU将执行它作为bsf
,对于非零输入产生相同的结果。新的CPU将执行它作为tzcnt
,在AMD上更有效(2个uops而不是7个)。英特尔将执行单个uop。如果您没有告诉GCC要针对特定的CPU进行优化,它将使用rep bsf
,也就是tzcnt
。pblendvb
(Intel上需要2个uop),可以获得更好的效果。当您实际想要一个标量int
时,该答案中的洗牌/水平缩减思路可能有用,作为vpermilps
的洗牌控制向量的元素索引,但似乎不太优化。int equal_first_dword_bitscan(__m128i x, __m128i y)
{
__m128i vcmp = _mm_cmpeq_epi32(x,y);
unsigned bitmask = _mm_movemask_ps(_mm_castsi128_ps(vcmp));
bitmask |= 1<<4; // return 4 if the low 4 bits are all 0
#ifdef __GNUC__
return __builtin_ctz(bitmask);
#else
return _tzcnt_u32( bitmask ); // runs as BSF on old CPUs, don't skip the OR
#endif
}
MSVC没有__builtin_ctz
,但即使您没有告诉它目标CPU支持BMI1,它也会编译_tzcnt_u32
。如果您确定只在支持BMI1的CPU上运行,则可以省略bitmask |= 1 << 4;
,以便返回32
表示未找到。
如果您在多个函数中使用尾随零计数,则最好将ifdef代码包装在一个帮助函数中,而不是在每个用例中都进行。
1
),则对该向量进行PCMPEQB操作,这样您就不需要在之后反转它。memcmp
查找不匹配字节位置一样。顺便提一下,汇编指令参考手册在每个条目底部列出了相关的C内嵌函数,您可以通过汇编助记符搜索Intel的内嵌函数查找器。(有关链接,请参见x86标签维基)。
最近我一直在编写“获取X的索引”SIMD算法。到目前为止,从比较掩码中提取索引的最通用方法是通过水平 索引 最小值。
这里是(无符号)整数的水平最小值:
int horizontal_min(__m128i x) {
x = _mm_min_epu32(x, _mm_shuffle_epi32(x, 0b01001110));
x = _mm_min_epu32(x, _mm_shuffle_epi32(x, 0b11100001));
return _mm_extract_epi32(x,0);
}
int equal_first(__m128i x, __m128i y) {
const __m128i index = _mm_set_epi32(0,1,2,3);
// Compute mask
__m128i mask = _mm_cmpeq_epi32(x,y);
// Select indices.
mask = _mm_blendv_epi8(_mm_set1_epi32(-1), index, mask);
// mask = index | (~mask);
// pick smallest indice.
return horizontal_min(mask);
}
phminposuw128
指令计算最小索引,则在使用16位索引时效率非常高。_mm_or_si128
,其中在要保留的元素中为false(0),在要拒绝的元素中为true(0xFFFFFFFF)。就像在这种情况下寻找任何非零元素一样,_mm_cmpeq_epi32(x,_mm_setzero_si128());
_mm_or_si128(index,mask);
然后您也可以放弃set1(4)
向量常量。(它本来就可以是-1,最高的无符号数字,这对于编译器来说更便宜,可以使用pcmpeqd xmm0,xmm0
实现。)pminud
也是SSE4.1,因此避免blendv不能仅使用SSE2完成此工作。 - Peter Cordesvpermilps
的洗牌控制。 - Peter Cordesequal_first
函数支持 tzcnt,那么可以使用 cmp/movemask/bit-scan 在 3 条单uop指令内实现(https://godbolt.org/z/Ks46E81Gh);否则,如果它可能在只有 bsf
的 CPU 上运行,则需要使用4个指令。当一个数上的低位都未被设置时,__builtin_ctz(bitmask | (1 << 4))
将返回 4
。 bsf
在 AMD CPU 上速度较慢,但是 rep bsf
可以在支持它的 CPU 上作为 tzcnt
运行,在不支持它的 CPU 上作为 bsf
运行,并且对于非零输入给出相同的结果。因此,使用 OR 技巧确保它始终为非零值。经过优化后,使用 set1(-1)
,你的函数需要8条指令和9个uops。 - Peter Cordesmovd
大约是2或3个周期的延迟,就像pmovmskb
一样。 因此大约需要9个周期。 我的是1(pcmpeqd)+ 3(pmovmskb)+ 1(or)+ Intel上的3(tzcnt / bsf)= Intel上的8个周期。 或者在AMD Zen上为7个周期,其中tzcnt
是2个uop,2c延迟(可能是位反转和lzcnt)。 (在AMD上,你的速度也快一个周期,其中vpblendvb
是单uop 1c延迟。) - Peter Cordes
LSB
移动到MSB
;(2) 对旋转后的数据进行VMOVMSKPS
。 - MarZzz