我正在寻找同一向量的分量之间的SSE按位或。 (编者注:这可能是一个X-Y问题,有关真正的比较逻辑,请参见下文。)
我正在从SPU内部函数移植一些SIMD逻辑。它有一条指令
spu_orx(a)
根据docs
spu_orx:或d = spu_orx(a)上的单词 向量a在逻辑上是Ored。结果在单词元素0中返回 向量d。 d的所有其他元素(1,2,3)被分配为 零。
我如何使用涉及最小指令的SSE 2-4来做到这一点? _mm_or_ps
是我到这里来的。
更新:
这是基于SPU的代码中的场景:
qword res = spu_orx(spu_or(spu_fcgt(x, y), spu_fcgt(z, w)))
因此,它首先对两个“更大”的比较进行“或”运算,然后对其结果进行“或”运算。 这些结果的后几对进行“与”运算,以获得最终比较值。
这实际上是在做(A||B||C||D||E||F||G||H) && (I||J||K||L||M||N||O||P) && ...
,其中A..D是fcgt(x,y)
的4x 32位元素,依此类推。
显然_mm_or_ps
的垂直_mm_cmp_ps
是减少到1个向量的好方法,但是那又是什么呢?随机+或其他?
更新1
关于“但是那又是什么?” 我执行
qword res = spu_orx(spu_or(spu_fcgt(x, y), spu_fcgt(z, w)))
在SPU上是这样的:
qword aRes = si_and(res, res1);
qword aRes1 = si_and(aRes, res2);
qword aRes2 = si_and(aRes1 , res3);
return si_to_uint(aRes2 );
多次在不同的输入上进行运算,然后将这些结果全部合并为一个结果,最后将其转换为整数0或1(假/真检验)
答案 0 :(得分:3)
bool any_nonzero = !_mm_testz_si128(v,v);
这将是对向量进行水平OR +将其布尔化为0/1整数的好方法。它将编译为多条指令,并且ptest same,same
本身为2 oups。但是一旦将结果作为标量整数,则标量AND
甚至比任何向量指令都便宜,并且您可以直接在结果上分支,因为它设置了整数标志。
#include <immintrin.h>
bool any_nonzero_bit(__m128i v) {
return !_mm_testz_si128(v,v);
}
On Godbolt,带有gcc9.1 -O3 -march = nehalem:
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锐龙ptest
仅1 uop,因此更好。
这里唯一的风险是,如果gcc或clang通过在对eax
进行AL运算之前不对x setcc
进行异或归零来创建错误的依赖关系。通常,gcc对花费额外的操作来打破错误的依赖关系非常狂热,所以我不知道为什么它不在这里。 (我确实与-march=skylake
和-mtune=generic
进行过核对,以防它依赖于-march=nehalem
的Nehalem部分寄存器重命名。甚至-march=znver1
都没有将它转换为零或零的EAX在ptest之前。)
如果我们可以避免使用_mm_or_ps
并让PTEST完成所有工作,那将是很好的。但是,即使我们考虑反转比较,垂直与/水平或的行为也无法让我们检查2个向量的所有8个元素,或这8个元素的任何。>
例如Can PTEST be used to test if two registers are both zero or some other condition?
// 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在评论中提出了建议。反转比较是一个好主意,对其他处理方式很有用。)
我们也许可以将水平ORing /布尔化延迟到组合了多个向量的某些结果之后。这样会使合并变得更加昂贵(imul
之类的东西),但在向量->整数级vs. PTEST方面节省了2 uop。
x86具有便宜的向量掩码->具有_mm_movemask_ps
的整数位图。特别是如果您最终想要基于结果,这可能是个好主意。 (但是x86也不具有||
指令来布尔化其输入,因此您不能仅仅&
来获得移动掩码结果。)
您可以做的一件事情是整数乘 movemask
的结果:x * y
是非零的,前提是两个输入都非零。与x & y
不同,对于0b0101 &
0b1010 for example. (Our inputs are 4-bit movemask results and
unsigned`可能为false,它是32位的,因此在溢出之前我们有一定的空间)。 AMD Bulldozer系列的整数乘法没有完全流水线化,因此这可能是旧AMD CPU的瓶颈。对于某些慢速64位乘法的低功耗CPU,仅使用32位整数也很合适。
尽管吞吐量movmskps
只能在一个端口上运行,但是如果吞吐量更多的是瓶颈而不是延迟,那么这可能会很好。
我不确定是否有任何便宜的整数运算可让我们稍后恢复逻辑AND结果。添加无效。即使只有一个输入为非零,结果也不为零。如果我们最终仅测试任何非零位,则将位连接在一起(移位+或)当然也像OR。我们不能只是按位AND,因为2 & 1 == 0
与2 && 1
不同。
4个元素的水平或运算需要多个步骤。
最明显的方法是_mm_movehl_ps
+ OR,然后是另一个shuffle + OR。 (请参见Fastest way to do horizontal float vector sum on x86,但将_mm_add_ps
替换为_mm_or_ps
)
但是,由于我们的输入是比较结果时,实际上并不需要精确的按位“或”运算,因此我们只关心是否有任何元素为非零。我们可以并且应该将向量视为整数,并查看诸如64位元素==
之类的整数指令。一个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。
或者,SSE4.1 _mm_minpos_epu16(cmp64)
(phminposuw
)会在1 uop(但有5个周期延迟)的情况下告诉我们,如果任一qword为零。在这种情况下,它将0
或0xFFFF
放在结果的最低字(16位)中。
如果我们颠倒了原始比较,则可以对此使用phminposuw
(不使用pcmpeqq
)来检查是否为零。因此,基本上,向量。 (假设它的元素为0 / -1)。我认为这对于反向输入是有用的结果。 (并且使我们免于使用_mm_xor_si128
翻转位)。
pcmpeqq
(_mm_cmpeq_epi64)的替代方法是对置零的向量进行SSE2 psadbw
,以在每个64位元素的底部获得0或非零结果。不过,它不会是面具,而是0xFF * 8
。尽管如此,它始终是0或0,因此您仍然可以对其进行运算。而且它不会反转。