SSE4.1 考试 bool any_nonzero = !_mm_testz_si128(v,v);
这将是水平 OR + 将向量布尔化为 0/1 整数的好方法。它将编译成多条指令,并且ptest same,same
它本身就是 2 微指令。但是,一旦您将结果作为标量整数,标量AND
甚至比任何向量指令都便宜,并且您可以直接在结果上进行分支,因为它设置了整数标志。
#include <immintrin.h>
bool any_nonzero_bit(__m128i v) {
return !_mm_testz_si128(v,v);
}
在使用 gcc9.1 -O3 -march=nehalem 的 Godbolt 上:
any_nonzero(long long __vector(2)):
ptest xmm0, xmm0 # 2 uops
setne al # 1 uop with false dep on old value of RAX
ret
对于整数寄存器中的单个位的水平或,这在 Intel 上只有 3 微秒。AMD Ryzenptest
只有 1 uop,所以它甚至更好。
这里唯一的风险是如果 gcc 或 clangeax
在执行setcc
到 AL 之前通过不进行异或归零来创建错误的依赖关系。通常 gcc 非常热衷于花费额外的微指令来打破错误的依赖关系,所以我不知道为什么它不在这里。(我确实检查过-march=skylake
它-mtune=generic
是否依赖于 Nehalem 的部分寄存器重命名-march=nehalem
。甚至-march=znver1
在 ptest 之前没有将它变为 xor-zero EAX。)
如果我们可以避免_mm_or_ps
并让 PTEST 完成所有工作,那就太好了。但是,即使我们考虑反转比较,垂直与/水平或行为也不能让我们检查 2 个向量的所有 8 个元素或这 8 个元素中的任何一个。
例如,PTEST 是否可以用于测试两个寄存器是否都为零或其他条件?
// NOT USEFUL
// 1 if all the vertical pairs AND to zero.
// but 0 if even one vertical AND result is non-zero
_mm_testz_si128( _mm_castps_si128(_mm_cmpngt_ps(x,y)),
_mm_castps_si128(_mm_cmpngt_ps(z,w)));
我提到这一点只是为了排除它并为您省去考虑这个优化想法的麻烦。(@chtz 在评论中提出了建议。反转比较是一个好主意,对其他做事方式很有用。)
没有 SSE4.1 / 延迟水平 OR
我们也许可以延迟水平 ORing / booleanizing 直到组合来自多个向量的一些结果。这使得组合更昂贵(imul
或其他东西),但在向量 -> 整数阶段与 PTEST 相比节省了 2 微秒。
x86 具有廉价的矢量掩码-> 整数位图和_mm_movemask_ps
. 尤其是如果您最终想要对结果进行分支,这可能是一个好主意。(但是 x86 也没有||
对其输入进行布尔化的指令,因此您不能只&
使用 movemask 结果)。
您可以做的一件事是整数相乘 movemask
结果:x * y
如果两个输入都非零,则非零。不像x & y
which can be false for 0b0101 &
0b1010 for example. (Our inputs are 4-bit movemask results and
unsigned` 是 32 位的,所以在溢出之前我们有一些空间)。AMD Bulldozer 系列具有未完全流水线化的整数乘法,因此这可能是旧 AMD CPU 的瓶颈。仅使用 32 位整数也适用于一些具有慢速 64 位乘法的低功耗 CPU。
如果吞吐量比延迟更多是瓶颈,这可能会很好,尽管movmskps
只能在一个端口上运行。
我不确定是否有任何更便宜的整数运算可以让我们稍后恢复逻辑与结果。添加不起作用;即使只有一个输入不为零,结果也不为零。如果我们最终只测试任何非零位,那么将这些位连接在一起(移位+或)当然也类似于 OR。我们不能只是按位与,因为2 & 1 == 0
不像2 && 1
。
保持在向量域中
4 个元素的水平 OR 需要多个步骤。
显而易见的方法是_mm_movehl_ps
+ OR,然后是另一个 shuffle + OR。(请参阅在 x86 上进行水平浮点向量求和的最快方法,但替换_mm_add_ps
为_mm_or_ps
)
但是由于当我们的输入是比较结果时,我们实际上并不需要精确的按位或,所以我们只关心是否有任何元素非零。我们可以并且应该将向量视为整数,并查看整数指令,例如 64-bit element ==
。一个 64 位元素覆盖/别名两个 32 位元素。
__m128i cmp = _mm_castps_si128(cmpps_result); // reinterpret: zero instructions
// SSE4.1 pcmpeqq 64-bit integer elements
__m128i cmp64 = _mm_cmpeq_epi64(cmp, _mm_setzero_si128()); // -1 if both elements were zero, otherwise 0
__m128i swap = _mm_shuffle_epi32(cmp64, _MM_SHUFFLE(1,0, 3,2)); // copy and swap, no movdqa instruction needed even without AVX
__m128i bothzero = _mm_and_si128(cmp64, swap); // both halves have the full result
bothzero
在此逻辑反转之后,将多个结果组合在一起将为您提供您正在寻找的多个条件的 AND。
或者,如果任一 qword 为零, SSE4.1 _mm_minpos_epu16(cmp64)
( phminposuw
)将在 1 uop(但 5 个周期延迟)内告诉我们。在这种情况下,它将放置在结果0
的0xFFFF
最低字(16 位)中。
如果我们反转原始比较,我们可以使用phminposuw
它(不带pcmpeqq
)来检查是否有零。 所以基本上是整个向量的水平与。(假设它是 0 / -1 的元素)。我认为这对于反向输入是一个有用的结果。(并且使我们免于使用_mm_xor_si128
翻转位)。
pcmpeqq
(_mm_cmpeq_epi64)的替代方案是 SSE2psadbw
对零向量,以在每个 64 位元素的底部获得 0 或非零结果。不过,它不会是面具,它是0xFF * 8
。尽管如此,它总是那个或 0,所以你仍然可以 AND 它。而且它不会反转。