3

我正在寻找相同向量的分量之间的 SSE 按位或。(编者注:这可能是一个 XY 问题,请参阅下面的真实比较逻辑。)

我正在从SPU内在函数移植一些 SIMD 逻辑。它有一个指令

spu_orx(a)

根据文档

spu_orx: OR word across d = spu_orx(a) 向量a的四个字元素进行逻辑或。结果在向量 d 的单词元素 0 中返回。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)))

因此,它首先对两个“更大”的比较进行 OR,然后对其结果进行 OR。这些结果中的后几对进行与运算以获得最终的比较值。

(A||B||C||D||E||F||G||H) && (I||J||K||L||M||N||O||P) && ...在 A..D 是 4x 32 位元素的情况下有效fcgt(x,y),依此类推。

显然,垂直_mm_or_ps_mm_cmp_ps结果是减少到 1 个向量的好方法,但是然后呢?Shuffle + OR,还是别的什么?

更新 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(假/真测试)

4

1 回答 1

3

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 & ywhich can be false for 0b0101 &0b1010 for example. (Our inputs are 4-bit movemask results andunsigned` 是 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 个周期延迟)内告诉我们。在这种情况下,它将放置在结果00xFFFF最低字(16 位)中。

如果我们反转原始比较,我们可以使用phminposuw它(不带pcmpeqq)来检查是否有零。 所以基本上是整个向量的水平与。(假设它是 0 / -1 的元素)。我认为这对于反向输入是一个有用的结果。(并且使我们免于使用_mm_xor_si128翻转位)。

pcmpeqq(_mm_cmpeq_epi64)的替代方案是 SSE2psadbw对零向量,以在每个 64 位元素的底部获得 0 或非零结果。不过,它不会是面具,它是0xFF * 8。尽管如此,它总是那个或 0,所以你仍然可以 AND 它。而且它不会反转。

于 2019-07-18T15:30:10.947 回答