在 arm neon 中有效地累积符号位

Posted

技术标签:

【中文标题】在 arm neon 中有效地累积符号位【英文标题】:Efficiently accumulate sign bits in arm neon 【发布时间】:2018-04-19 10:25:59 【问题描述】:

我有一个循环进行一些计算,然后将符号位存储到一个向量中:

uint16x8_t rotate(const uint16_t* x);

void compute(const uint16_t* src, uint16_t* dst)

    uint16x8_t sign0 = vmovq_n_u16(0);
    uint16x8_t sign1 = vmovq_n_u16(0);
    for (int i=0; i<16; ++i)
    
        uint16x8_t r0 = rotate(src++);
        uint16x8_t r1 = rotate(src++);
        // pseudo code:
        sign0 |= (r0 >> 15) << i;
        sign1 |= (r1 >> 15) << i;
    
    vst1q_u16(dst+1, sign0);
    vst1q_u16(dst+8, sign1);

在伪代码之后的 neon 中累积符号位的最佳方法是什么?

Here's what I came up with:

    r0 = vshrq_n_u16(r0, 15);
    r1 = vshrq_n_u16(r1, 15);
    sign0 = vsraq_n_u16(vshlq_n_u16(r0, 15), sign0, 1);
    sign1 = vsraq_n_u16(vshlq_n_u16(r1, 15), sign1, 1);

另外,请注意,“伪代码”实际上可以正常工作,并且生成几乎相同的代码。这里有什么可以改进的?注意,在实际代码中,循环中没有函数调用,我精简了实际代码以使其易于理解。 另一点:在霓虹灯中,您不能使用变量进行矢量移位(例如,i 不能用于指定移位次数)。

【问题讨论】:

vsraq 是算术移位,而不是逻辑移位?为什么用那个?此外,如果在移位前使用 AND 将非符号位归零,则可以使用更少的移位。比如sign0 |= (r0 &amp; 0x8000) &gt;&gt; (15-i); 或者固定班次:sign0 |= (r0 &amp; 0x8000); sign0 &gt;&gt;= 1; 后者用SIMD实现应该很简单高效,但我不太了解ARM。 VSRA 是向量右移立即值和累加。 _u16_s16 将定义逻辑与算术移位。 哦,所以你用(x &gt;&gt; 15) &lt;&lt; 15而不是x &amp; 0x8000来隔离符号位。看起来您可以使用vsra 轻松实施我之前评论中的第二条建议:tmp = r0 &amp; 0x8000; sign0 = (sign0 &gt;&gt; 1) + tmp; for & 0x8000 我需要创建一个 q 寄存器,但我不确定我是否有足够的循环。我会试试看我是否能得到更好的结果。两个班次并不复杂,除了霓虹灯中的 AFAIK 只有一个班次单元,但编译器在生成的代码中传播班次。 你能不能把它作为答案,我认为它应该比我的代码更好 【参考方案1】:

ARM 可以在一条 vsri 指令中执行此操作(感谢 @Jake'Alquimista'LEE)。

给定一个新向量,您希望从中获取符号位,将每个元素的低 15 位替换为累加器右移 1。

你应该展开 2,这样编译器就不需要 mov 指令将结果复制回同一个寄存器,因为 vsri 是一个 2 操作数指令,我们需要在这里使用它的方式在与旧的 sign0 累加器不同的寄存器中为我们提供结果。

sign0 =  vsriq_n_u16(r0, sign0, 1);
// insert already-accumulated bits below the new bit we want

在 15 次插入后(或者 16 次,如果您从 sign0 = 0 开始而不是剥离第一次迭代并使用 sign0=r0),sign0 的所有 16 位(每个元素)将是来自 r0 值的符号位.


之前的建议:用一个向量常数来隔离符号位。比两班倒更有效率。

您使用 VSRA 累加来移动累加器并添加新位的想法很好,所以我们可以保留它并减少总共 2 条指令。

tmp = r0 & 0x8000;            // VAND
sign0 = (sign0 >> 1) + tmp;   // VSRA

或使用霓虹内在函数:

uint16x8_t mask80 = vmovq_n_u16(0x8000);
r0 = vandq_u16(r0, mask80);        // VAND
sign0 = vsraq_n_u16(r0, sign0, 1); // VSRA

用你喜欢的内在函数或 asm 实现,并以相同的方式编写标量版本,以使编译器有更好的机会进行自动矢量化。


这确实需要寄存器中的向量常数。如果您对寄存器非常严格,那么 2 班可能会更好,但总共 3 班似乎可能会成为移位器吞吐量的瓶颈,除非 ARM 芯片通常在 SIMD 桶式移位器上花费大量空间。

在这种情况下,也许可以使用这种通用 SIMD 思想,无需 ARM shift+accumulate 或 shift+insert

tmp = r0 >> 15;     // logical right shift
sign0 += sign0;     // add instead of left shifting
sign0 |= tmp;       // or add or xor or whatever.

这会以相反的顺序为您提供位。如果你能以相反的顺序生产它们,那就太好了。

否则,ARM 是否有 SIMD 位反转或仅用于标量? (以相反的顺序生成并在最后翻转它们,每个矢量位图都有一些额外的工作,希望只有一条指令。)

更新:是的,AArch64 有rbit,所以你可以反转一个字节内的位,然后进行字节洗牌以将它们按正确的顺序排列。 x86 可以使用pshufb LUT 在两个 4 位块中的字节内进行位反转。不过,当您在 x86 上累积位时,这可能不会在做更多工作之前出现。

【讨论】:

有趣的是,no matter what I try clang 生成了相同的代码,就好像它理解了我的意图并以不同的方式实现了结果。但是,这确实适用于gcc and produces much better code。 @Pavel:可能clang 只是将vsraq 内在函数编译到其所涉及操作的内部表示中,但是在生成代码时未能将它们组合成vsra asm 指令。在为 x86 编译时,它通常使用与源内部函数不同的 shuffle,因为它将源代码视为源代码并应用“as-if”规则来获得等效的结果。通常这在 x86 上很好,但在用内在函数表示时确实会破坏一些优化。看来 clang 不知道如何为 ARM 编写好的代码。 哦,现在说得通了,为什么它会这样做。顺便说一句,您的第二个建议实际上可能有效,在 arm64 上有 RBIT 可以反转位(但仅适用于字节)。 vsri 更好,因为它会自动覆盖右侧的位,因此您既不需要 and 操作也不需要常量 0x8000。如果您不介意,我会发布我认为最有效的代码。 @Jake'Alquimista'LEE:感谢您的建议,vsri 看起来很完美。更新了我的答案,但随时发布您自己的性能分析或优化建议,或编辑我的。

以上是关于在 arm neon 中有效地累积符号位的主要内容,如果未能解决你的问题,请参考以下文章

是否有 ARM NEON 指令用于该轮向零的有符号右移?

无符号字符图像上的快速高斯模糊 - ARM Neon Intrinsics - iOS Dev

ARM Neon:用于减法的 VPADAL

如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?

如何使用缩放有效地将 16 位无符号短转换为 8 位无符号字符?

ARM NEON 汇编和浮点舍入