【发布时间】: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 & 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;
}
【问题讨论】:
-
UInt256是如何在别处使用的,即这些值是否更可能预先存在于 SIMD 寄存器、通用寄存器或内存中?我想cmp和 3ccmps 的开销可能比一堆 SIMD 寄存器杂耍的开销要少,但是不得不溢出一堆 GP 寄存器并加载值可能会以另一种方式倾斜平衡。我怀疑整体效率问题最好通过基准测试来回答,因为它也会受到其他代码(寄存器压力、缓存使用等)的影响 -
它们之前在内存中,并加载了“ld1”。
标签: arm comparison simd neon arm64