Eta*_*tan 5 comparison arm simd neon arm64
我想有效地比较两个little-endian 256位值和A64 Neon指令(asm).
平等(=)
为了平等,我已经有了一个解决方案:
bool eq256(const UInt256 *lhs, const UInt256 *rhs) {
bool result;
Run Code Online (Sandbox Code Playgroud)
首先,将两个值加载到SIMD寄存器中.
__asm__("ld1.2d { v0, v1 }, %1 \n\t"
"ld1.2d { v2, v3 }, %2 \n\t"
Run Code Online (Sandbox Code Playgroud)
将值的每个64位肢体相互比较.对于那些相等的肢体,这导致-1(所有位设置),如果位不同,则导致0(所有位清零).
"cmeq.2d v0, v0, v2 \n\t"
"cmeq.2d v1, v1, v3 \n\t"
Run Code Online (Sandbox Code Playgroud)
将2个向量的结果减少到1个向量,只保留包含"0(所有位清除)"的向量,如果有的话.
"uminp.16b v0, v0, v1 \n\t"
Run Code Online (Sandbox Code Playgroud)
将结果从1个向量减少到1个字节,如果有的话,只保留一个带零的字节.
"uminv.16b b0, v0 \n\t"
Run Code Online (Sandbox Code Playgroud)
移至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;
}
Run Code Online (Sandbox Code Playgroud)
问题
这比使用普通的旧ARM寄存器进行4次比较更有效吗?
有没有办法进一步优化这个?我认为我浪费了很多周期只是为了将整个向量减少到单个标量布尔值.
小于比较(<)
让我们将两个整数表示为64位肢体(little-endian)的元组:
然后,lhs <rhs,如果此计算结果为true:
(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)
Run Code Online (Sandbox Code Playgroud)
SIMD指令现在可用于一次评估多个操作数.假设(l1,l2),(l3,l4),(r1,r2),(r3,r4)是存储两个256位数的方式,我们可以很容易地得到所有需要的值(有用的值在胆大):
问题
更新
我刚刚为"不到"打了一个工作实现.
基本上,我用重复条件替换了上面的1,因为A & A == A & 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;
}
Run Code Online (Sandbox Code Playgroud)
使用基于 Swift 的测试运行器使用 XCTestmeasureMetrics 进行基准测试。分配了两个 256 位 Int。然后,对相同的两个 int 重复操作 1 亿次,停止测量,并使用 arc4random 为这两个 int 的每个分支分配一个新的随机值。第二次运行是在附加仪器的情况下执行的,并且每条指令的 CPU 时间分布都以注释形式记录在其旁边。
平等(==)
为了平等,当结果从 SIMD 寄存器传输回 ARM 寄存器时,SIMD 似乎会丢失。SIMD 可能仅在结果用于进一步的 SIMD 计算中或使用长于 256 位的整数时才值得(ld1 似乎比 ldp 更快)。
单指令多数据流
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")
Run Code Online (Sandbox Code Playgroud)
测量的[时间,秒]平均值:11.558,相对标准偏差:0.065%,值:[11.572626、11.560558、11.549322、11.568718、11.558530、11.550490、11.557086、11.551803、11.55 7529, 11.549782]
标准
获胜者在这里。该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;
Run Code Online (Sandbox Code Playgroud)
测量的[时间,秒]平均值:11.146,相对标准偏差:0.034%,值:[11.149754、11.142854、11.146840、11.149392、11.141254、11.148708、11.142293、11.150491、11.13 9593, 11.145873]
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];
Run Code Online (Sandbox Code Playgroud)
编译为
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%
Run Code Online (Sandbox Code Playgroud)
测量的[时间,秒]平均值:11.531,相对标准偏差:0.040%,值:[11.525511、11.529820、11.541940、11.531776、11.533287、11.526628、11.531392、11.526037、11.53 1784, 11.533786]
小于 (<)
(待定 - 稍后更新)