我想有效地将两个小端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次比较更有效吗?
有没有办法进一步优化?我认为我浪费了很多周期只是为了将整个向量减少为单个标量布尔值。
小于比较(<)
让我们将两个整数表示为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)
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;
}
答案 0 :(得分:3)
使用基于Swift的测试运行器对XCTest measureMetrics进行基准测试。分配了两个256位Int。然后,在相同的两个整数上重复操作1亿次,停止测量,并且为两个整数的每个肢体分配具有arc4随机的新随机值。第二次运行是在连接了Instruments的情况下执行的,并且每条指令都会记录CPU时间分布,作为旁边的注释。
平等(==)
为了相等,当结果从SIMD寄存器传回ARM寄存器时,SIMD似乎丢失了。当结果用于进一步的SIMD计算时,或者如果使用的长度超过256位(ld1似乎比ldp快),SIMD可能是值得的。
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.551803,11.557529,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;
测量[时间,秒]平均值:11.146,相对标准偏差:0.034%,值:[11.149754,11.142854,11.146840,11.149392,11.141254,11.148708,11.142293,11.150491,11.139593,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];
编译为
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,11.531784,11.533786]
小于(&lt;)
(待定 - 稍后会更新)