【问题标题】:Optimizing horizontal boolean reduction in ARM NEON优化 ARM NEON 中的水平布尔减少
【发布时间】:2015-09-20 17:30:09
【问题描述】:

我正在尝试使用跨平台 SIMD 库 ala ecmascript_simd aka SIMD.js,其中一部分是提供一些“水平”SIMD 操作。特别是,库提供的 API 包括 any(<boolN x M>) -> boolall(<boolN x M>) -> bool 函数,其中 <T x K>K 类型为 T 的元素的向量,boolNN 位布尔值,即所有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 这样的指令。有一种方法是简单地提取每个元素并使用标量进行计算。例如。有一种简单的方法,但也有使用 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 个点进行操作,并在所有点都超出范围时退出内部循环......这需要进行比较,然后进行水平与。

【问题讨论】:

  • movemskps 的更有趣的 SSE 指令是 ptest。您可以将其用于andor。我认为 Neon 有相同的指令vtest。我还没有实现这个,但我想你可以在这里找到你的答案fastest-way-to-test-a-128-bit-neon-register-for-a-value-of-0-using-intrinsics
  • @Zboson: vtst 在这里并不是特别有用,遗憾的是(因为您已经从比较中获得了一个 0/​​-1 值的向量)。 Nils 来自链接答案的建议(饱和添加 + 读取 Q 位)通常效果不佳,因为 Q 位是粘性的,因此您需要先使用 RMW 清除它。所以通常的方法是在 arm32 上使用多个 vpmax/vpmin 在 arm64 上使用单个 umaxv/uminv
  • 我不知道许多“mandlebrot 的 SIMD 实现将同时在 4 个点上运行,并且当所有这些点都超出范围时退出内部循环......”我一直在做这一段时间我自己(实际上是 8 像素,带有 AVX 用于单浮点)。对于 x86,我使用 ptest,但您似乎已经找到了 ARM 的最佳解决方案:即 min/max 两次使用 arm7,一次使用 arm8。
  • @StephenCanon,在这种情况下,也许您可​​以向fastest-way-to-test-a-128-bit-neon-register-for-a-value-of-0-using-intrinsics 提供答案。
  • 相关:NEON pack vector compare result into bitmap 要求 movmskps 等效项。但是,可能不是测试任何元素是否为真之类的正确构建块。 (例如,只打包到 4 个字节而不是 4 位可能更容易,并且测试 0 或 -1 的 32 位整数)

标签: arm simd neon


【解决方案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 - 有一个解决方案。

【讨论】:

  • rev/clz 在 clz 方面实现计数 trailing 个零,因为 ARM/AArch64 没有 x86 bsf / tzcnt 的直接等效项。 (这是您想要在 x86 或 AArch64 等小端系统上找到向量中最低地址匹配的内容。)RBIT / CLZ 是 __builtin_ctz 的标准实现/仿真,但如果您知道一个字节中的所有位是相同的,那么 REV 也可以工作。其余大部分代码只是对 +8 或 +0 进行无分支处理,以匹配您在第 1 部分中描述的数据部分。(cinc / csinc 只能加 1,不能加 8,所以加 / csel 很好。)
  • 对 32 位 ARM 代码不使用类似功能的一个原因是,当整数指令从 SIMD 寄存器读取值时,某些 ARM 流水线会出现很大的停顿。因此,即使您可以在几条指令中对 SIMD 比较进行分支,也可能会更慢。
  • @PeterCordes 对于 armv7 的 reverse 想法怎么样?这样好吗?
  • IDK,我自己对 ARM 不是很熟悉。 Perform a horizontal logical/bitwise AND operation across all lanes of uint8x8 Neon vector 我认为是相关的; Jake 发布了许多很好的 ARM asm 答案,其中包含类似的有用技巧。包括 NEON pack vector compare result into bitmap 来模拟 x86 movmskps(4x 4 字节元素 -> 4 位)。
【解决方案2】:

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

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

对所有事情都持保留态度 - 我还没有设置 arm 基准

注意:除了基本的 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 字节寄存器似乎更好。

然后在该双字寄存器上执行所有/任何操作。

ARM-v8,8 字节

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

ARM-v8,16 字节

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

结论

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-07
    • 1970-01-01
    • 2013-02-01
    • 1970-01-01
    • 2013-09-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多