【问题标题】:A64 Neon SIMD - 256-bit comparisonA64 Neon SIMD - 256 位比较
【发布时间】:2015-06-26 21:08:32
【问题描述】:

我想有效地将​​两个 little-endian 256 位值与 A64 Neon 指令 (asm) 进行比较。


平等 (=)

为了平等,我已经有了解决办法:

bool eq256(const UInt256 *lhs, const UInt256 *rhs) {
    bool result;

首先,将这两个值加载到 SIMD 寄存器中。

    __asm__("ld1.2d    { v0, v1 }, %1    \n\t"
            "ld1.2d    { v2, v3 }, %2    \n\t"

将值的每个 64 位分支相互比较。这导致那些相等的肢体为 -1(所有位设置),如果位不同,则为 0(所有位清除)。

            "cmeq.2d   v0, v0, v2        \n\t"
            "cmeq.2d   v1, v1, v3        \n\t"

将结果从 2 个向量减少到 1 个向量,如果有的话,只保留包含“0(所有位清除)”的那个。

            "uminp.16b v0, v0, v1        \n\t"

将结果从 1 个向量减少到 1 个字节,如果有的话,只保留一个带零的字节。

            "uminv.16b b0, v0            \n\t"

移动到 ARM 寄存器,然后与 0xFF 比较。这就是结果。

            "umov      %w0, v0.b[0]      \n\t"
            "cmp       %w0, 0xFF         \n\t"
            "cset      %w0, eq               "
            : "=r" (result)
            : "m" (*lhs->value), "m" (*rhs->value)
            : "v0", "v1", "v2", "v3", "cc");
    return result;
}

问题

  • 这是否比与普通的旧 ARM 寄存器进行 4 次比较更有效?

    • 例如是否有引用不同操作时间的来源?我在 iPhone 5s 上这样做。
  • 有没有办法进一步优化?我认为我浪费了很多周期只是为了将整个向量减少为单个标量布尔值。


小于比较 (

让我们将两个整数表示为 64 位肢体元组(小端):

  • lhs = (l0, l1, l2, l3)
  • rhs = (r0, r1, r2, r3)

那么,lhs

(l3 < r3) &     1     &     1     &     1     |
(l3 = r3) & (l2 < r2) &     1     &     1     |
(l3 = r3) & (l2 = r2) & (l1 < r1) &     1     |
(l3 = r3) & (l2 = r2) & (l1 = r1) & (l0 < r0)

SIMD 指令现在可用于一次计算多个操作数。假设 (l1, l2), (l3, l4), (r1, r2), (r3, r4) 是两个 256 位数字的存储方式,我们可以很容易地得到所有需要的值(有用的值在粗体):

  • cmlo.2d => (l1 , (l2
  • cmlo.2d => (l3 , (l4
  • cmeq.2d => (l1 = r1), (l2 = r2)
  • cmeq.2d => (l3 = r3), (l4 = r4)

问题

  • 有了四个 SIMD 寄存器中的这些值,我现在想知道应用 & 和 | 的最佳策略是什么运算符,然后将其简化为单个布尔值。

更新

我刚刚为“小于”拼凑了一个有效的实现。

基本上,我用重复的条件替换了上面的 1,因为A &amp; A == A &amp; 1

然后,我在我的矩阵中布置三个 2x2 正方形,并按位与它们。 现在,我使用按位 OR 进行归约 - 首先从两个向量到一个向量,然后到一个字节,然后复制到 ARM 寄存器,并测试 0xFF。与上述相等的模式相同。

上述问题仍然有效。我不确定代码是否是最优的,并且想知道我是否错过了一些通用的 SIMD 模式来更有效地做这些事情。另外:对于这种情况,当输入操作数来自内存时,NEON 是否值得?

bool lt256(const UInt256 *lhs, const UInt256 *rhs) {
    bool result;
    __asm__(// (l3 < r3) & (l3 < r3) |
            // (l3 = r3) & (l2 < r2) |
            // (l3 = r3) & (l2 = r2) & (l1 < r1) & (l1 < r1) |
            // (l3 = r3) & (l2 = r2) & (l1 = r1) & (l0 < r0)

            "ld1.2d    { v0, v1 }, %1    \n\t"
            "ld1.2d    { v2, v3 }, %2    \n\t"

            // v0: [ l3 = r3 ] [ l2 = r2 ]
            // v1: [ l0 < r0 ] [ l1 < r1 ]
            // v2: [ l0 = r0 ] [ l1 = r1 ]
            // v3: [ l2 < r2 ] [ l3 < r3 ]
            // v4: [ l2 = r2 ] [ l3 = r3 ]
            "cmeq.2d   v4, v1, v3        \n\t"
            "cmlo.2d   v3, v1, v3        \n\t"
            "cmlo.2d   v1, v0, v2        \n\t"
            "cmeq.2d   v2, v0, v2        \n\t"
            "ext.16b   v0, v4, v4, 8     \n\t"

            // v2: [ l1 < r1 ] [ l1 = r1 ]
            // v1: [ l1 < r1 ] [ l0 < r0 ]
            "trn2.2d   v2, v1, v2        \n\t"
            "ext.16b   v1, v1, v1, 8     \n\t"

            // v1: [ l1 < r1  &  l1 < r1 ] [ l1 = r1  &  l0 < r0 ]
            "and.16b   v1, v2, v1        \n\t"

            // v2: [ l3 < r3 ] [ l3 = r3 ]
            // v3: [ l3 < r3 ] [ l2 < r2 ]
            "ext.16b   v2, v3, v0, 8     \n\t"
            "ext.16b   v3, v3, v3, 8     \n\t"

            // v3: [ l3 < r3  &  l3 < r3 ] [ l3 = r3  &  l2 < r2 ]
            "and.16b   v3, v2, v3        \n\t"

            // v2: [ l3 = r3 ] [ l3 = r3 ]
            // v4: [ l2 = r2 ] [ l2 = r2 ]
            "ext.16b   v2, v4, v0, 8     \n\t"
            "ext.16b   v4, v0, v4, 8     \n\t"

            // v2: [ l3 = r3  &  l2 = r2 ] [ l3 = r3  &  l2 = r2 ]
            "and.16b   v2, v2, v4        \n\t"

            // v1: [ l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 ]
            //     [ lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ]
            "and.16b   v1, v2, v1        \n\t"

            // v1: [ l3 < r3 & l3 < r3 |
            //       l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 ]
            //     [ l3 = r3 & l2 < r2 |
            //       lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ]
            "orr.16b   v1, v3, v1        \n\t"

            // b1: [ l3 < r3 & l3 < r3 |
            //       l3 = r3 & l2 = r2 & l1 < r1 & l1 < r1 |
            //       l3 = r3 & l2 < r2 |
            //       lr = r3 & l2 = r2 & l1 = r1 & l0 < r0 ]
            "umaxv.16b b1, v1            \n\t"
            "umov      %w0, v1.b[0]      \n\t"
            "cmp       %w0, 0xFF         \n\t"
            "cset      %w0, eq"
            : "=r" (result)
            : "m" (*lhs->value), "m" (*rhs->value)
            : "v0", "v1", "v2", "v3", "v4", "cc");
    return result;
}

【问题讨论】:

  • UInt256 是如何在别处使用的,即这些值是否更可能预先存在于 SIMD 寄存器、通用寄存器或内存中?我想cmp 和 3 ccmps 的开销可能比一堆 SIMD 寄存器杂耍的开销要少,但是不得不溢出一堆 GP 寄存器并加载值可能会以另一种方式倾斜平衡。我怀疑整体效率问题最好通过基准测试来回答,因为它也会受到其他代码(寄存器压力、缓存使用等)的影响
  • 它们之前在内存中,并加载了“ld1”。

标签: arm comparison simd neon arm64


【解决方案1】:

使用基于 Swift 的测试运行程序使用 XCTest measureMetrics 进行基准测试。分配了两个 256 位 Int。然后,对相同的两个 int 重复操作 1 亿次,停止测量,并为两个 int 的每个肢体分配一个新的随机值 arc4random。第二次运行是在附加仪器的情况下执行的,每条指令的 CPU 时间分布都会作为注释记录在它旁边。


平等(==)

对于相等性,当结果从 SIMD 寄存器传输回 ARM 寄存器时,SIMD 似乎丢失了。 SIMD 可能仅在将结果用于进一步的 SIMD 计算时才值得,或者如果使用比 256 位更长的整数(ld1 似乎比 ldp 快)。

  • SIMD

    bool result;
    __asm__("ld1.2d    { v0, v1 }, %1    \n\t"    //  5.1%
            "ld1.2d    { v2, v3 }, %2    \n\t"    // 26.4%
            "cmeq.2d   v0, v0, v2        \n\t"
            "cmeq.2d   v1, v1, v3        \n\t"
            "uminp.16b v0, v0, v1        \n\t"    //  4.0%
            "uminv.16b b0, v0            \n\t"    // 26.7%
            "umov      %w0, v0.b[0]      \n\t"    // 32.9%
            "cmp       %w0, 0xFF         \n\t"    //  0.0%
            "cset      %w0, eq               "
            : "=r" (result)
            : "m" (*lhs->value), "m" (*rhs->value)
            : "v0", "v1", "v2", "v3", "cc");
    return result;                                //  4.9% ("ret")
    

    测量的[时间,秒]平均值:11.558,相对标准偏差:0.065%,值:[11.572626, 11.560558, 11.549322, 11.568718, 11.558530, 11.550490, 11.557086, 11.57525, 11.57525, 11.57525, 11.57525]

  • 标准

    这里的赢家。 ccmp 指令真的在这里大放异彩:-) 不过很明显,问题出在内存限制上。

    bool result;
    __asm__("ldp       x8, x9, %1          \n\t"  // 33.4%
            "ldp       x10, x11, %2        \n\t"
            "cmp       x8, x10             \n\t"
            "ccmp      x9, x11, 0, eq      \n\t"
            "ldp       x8, x9, %1, 16      \n\t"  // 34.1%
            "ldp       x10, x11, %2, 16    \n\t"
            "ccmp      x8, x10, 0, eq      \n\t"  // 32.6%
            "ccmp      x9, x11, 0, eq      \n\t"
            "cset      %w0, eq             \n\t"
            : "=r" (result)
            : "m" (*lhs->value), "m" (*rhs->value)
            : "x8", "x9", "x10", "x11", "cc");
    return result;
    

    测量的 [时间,秒] 平均值:11.146,相对标准偏差:0.034%,值:[11.149754, 11.142854, 11.146840, 11.149392, 11.141254, 11.148708, 11.142293, 11.1850491, 11.150491, 11.11]p]

  • C

    LLVM 未能检测到“ccmp”是在这里使用的好指令,并且比上面的 asm 版本慢。

    return
        lhs->value[0] == rhs->value[0] &
        lhs->value[1] == rhs->value[1] &
        lhs->value[2] == rhs->value[2] &
        lhs->value[3] == rhs->value[3];
    

    编译为

    ldp    x8, x9, [x0]                           // 24.1%
    ldp    x10, x11, [x1]                         //  0.1%
    cmp    x8, x10                                //  0.4%
    cset   w8, eq                                 //  1.0%
    cmp    x9, x11                                // 23.7%
    cset   w9, eq
    and    w8, w8, w9                             //  0.1%
    ldp    x9, x10, [x0, #16]
    ldp    x11, x12, [x1, #16]                    // 24.8%
    cmp    x9, x11
    cset   w9, eq                                 //  0.2%
    and    w8, w8, w9
    cmp    x10, x12                               //  0.3%
    cset   w9, eq                                 // 25.2%
    and    w0, w8, w9
    ret                                           //  0.1%
    

    测量的[时间,秒]平均值:11.531,相对标准偏差:0.040%,值:[11.525511, 11.529820, 11.541940, 11.531776, 11.533287, 11.526628, 11.531392, 11.526037, 1.53854, 1.5384, 11.5385]


小于 (

(待定 - 稍后会更新)

【讨论】:

  • 干得好!在ccmp 变体中,您可能会通过首先加载和测试最重要的对,然后在比较失败时完全跳过第二组加载来节省一些宝贵的内存带宽。
  • 我想要恒定的运行时间并且没有依赖数据的内存访问。
猜你喜欢
  • 2016-11-04
  • 2012-02-22
  • 2012-03-09
  • 2015-08-13
  • 2018-03-16
  • 2013-02-20
  • 1970-01-01
  • 2014-09-11
  • 1970-01-01
相关资源
最近更新 更多