优化ARM NEON

时间:2015-07-03 01:40:42

标签: arm simd neon

我尝试使用跨平台的SIMD库ala ecmascript_simd aka SIMD.js,其中一部分是提供了一些水平的&#34;水平&#34; SIMD操作。特别是,库提供的API包括any(<boolN x M>) -> boolall(<boolN x M>) -> bool个函数,其中<T x K>K类型TboolN元素的向量}是一个N - 位布尔值,即全部为1或全部为零,因为SSE和NEON返回进行比较操作。

例如,让v<bool32 x 4>(128位向量),它可能是VCLT.S32或其他内容的结果。我想计算all(v) = v[0] && v[1] && v[2] && v[3]any(v) = v[0] || v[1] || v[2] || v[3]

使用SSE很容易,例如movmskps将提取每个元素的高位,因此上面类型的all变为(使用C内在函数):

#include<xmmintrin.h>
int all(__m128 x) {
    return _mm_movemask_ps(x) == 8 + 4 + 2 + 1;
}

,同样适用于any

我很难找到明显/好的/有效的方法来实现NEON,它不支持像movmskps这样的指令。有简单地提取每个元素并使用标量计算的方法。例如。这是天真的方法,但也有使用&#34;水平&#34; NEON支持的操作,如VPMAX and VPMIN

#include<arm_neon.h>

int all_naive(uint32x4_t v) {
    return v[0] && v[1] && v[2] && v[3];
}
int all_horiz(uint32x4_t v) {
    uint32x2_t x = vpmin_u32(vget_low_u32(v),
                             vget_high_u32(v));
    uint32x2_t y = vpmin_u32(x, x);
    return x[0] != 0;
}

(人们可以用VPADD为后者做类似的事情,这可能会更快,但它基本上是相同的想法。)

是否还有其他技巧可用于实现此目的?

是的,我知道SIMD矢量单元的水平操作不是很好。但有时它很有用,例如mandlebrot的许多SIMD实现将同时在4个点上运行,并且当所有这些都超出范围时保释出内循环...这需要进行比较然后进行水平和。

2 个答案:

答案 0 :(得分:2)

这是我目前在 eve library 中实施的解决方案。

如果您的后端支持 C++20,您可以使用该库:它具有 arm-v7、arm-v8(目前只有小端)和从 sse2 到 avx-512 的所有 x86 的实现。它是开源的,并获得 MIT 许可。目前处于测试阶段。如果您正在试用该库,请随时联系(例如遇到问题)。

持保留态度 - 我还没有设置手臂基准

注意:在基本的 all 和 any 之上,我们还有一个 movemask 等效于执行更复杂的操作,例如 first_true。这不是问题的一部分,这并不奇怪,但可以找到代码 here

ARM-V7,8 字节寄存器

现在,arm-v7 是 32 位架构,所以我们尝试尽可能使用 32 位元素。

  • 任何

最多使用成对 32 位。如果任何元素为真,则最大值为真。

// cast to dwords
dwords = vpmax_u32(dwords, dwords);
return vget_lane_u32(dwords, 0);
  • 全部

成对的最小值而不是最大值。还有你针对变化测试的内容。 如果您有 4 字节元素 - 只需测试是否为真。如果是短裤或字符 - 你需要测试 -1;

// cast to dwords
dwords = vpmin_u32(dwords, dwords);
std::uint32_t combined = vget_lane_u32(dwords, 0);

// Assuming T is your scalar type
if constexpr ( sizeof(T) >= 4 ) return combined;

// I decided that !~ is better than -1, compiler will figure it out.
return !~combined; 

ARM-V7,16 字节寄存器

对于大于字符的任何内容,只需转换为 64 位。这是 vector narrow integer 次转化的列表。

对于字符,我发现最好的方法是重新解释为 uint32 并进行额外检查。 所以比较所有的 == -1 和 > 0 的任何。 拆分成两个 8 字节寄存器似乎更好。

然后在那个双字寄存器上做 all/any。

ARM-v8,8 字节

ARM-v8 支持 64 位,因此您可以获得 64 位通道。那个是可以简单测试的。

ARM-v8,16 字节

我们使用 vmaxvq_u32 因为 anyvminvq_u32vminvq_u16vminvq_u8 没有 64 位的 all,具体取决于元素大小。 (类似于glibc strlen

结论

缺乏基准肯定让我担心,有些指令有时会出现问题,我对此一无所知。 无论如何,这是我所拥有的最好的,至少到目前为止是这样。

答案 1 :(得分:1)

注意:今天第一次看胳膊,我可能是错的。

UPD:删除了 ARM-V7,并将在单独的答案中写下我们最终做了什么

ARM-V8。

对于 ARM-V8,请查看 glibc 中的 strlen 实现: https://code.woboq.org/userspace/glibc/sysdeps/aarch64/multiarch/strlen_asimd.S.html

ARM-V8 引入了跨寄存器的缩减。这里他们使用 min 与 0 进行比较

        uminv        datab2, datav.16b
        mov          tmp1, datav2.d[0]
        cbnz         tmp1, L(main_loop)

找到最小的字符,与 0 比较 - 取接下来的 16 个字节。

ARM-V8 中还有其他一些减少,例如 vaddvq_u8
我敢肯定,您可以使用 movemask 完成大部分您想做的事情。

这里另一个有趣的事情是他们如何找到 first_true

        /* Set te NULL byte as 0xff and the rest as 0x00, move the data into a
           pair of scalars and then compute the length from the earliest NULL
           byte.  */
        cmeq        datav.16b, datav.16b, #0
        mov        data1, datav.d[0]
        mov        data2, datav.d[1]
        cmp        data1, 0
        csel        data1, data1, data2, ne
        sub        len, src, srcin
        rev        data1, data1
        add        tmp2, len, 8
        clz        tmp1, data1
        csel        len, len, tmp2, ne
        add        len, len, tmp1, lsr 3

看起来有点吓人,但我的理解是:

  1. 他们只是通过执行 if/else 将其缩小到 64 位数字(如果前半部分没有零 - 后半部分有。
  2. 使用计数前导零来查找位置(不太了解这里的所有字节顺序,但它是 libc - 所以这是正确的)。

所以 - 如果您只需要 V8 - 有一个解决方案。