ARM NEON 优化中的建议

Posted

技术标签:

【中文标题】ARM NEON 优化中的建议【英文标题】:Suggestion in ARM NEON optimization 【发布时间】:2013-07-05 14:08:17 【问题描述】:

出于学术目的,我想尝试为以下算法编写一个 ARM NEON 优化,甚至只是为了测试是否有可能获得任何性能改进。我认为这不是 SIMD 优化的好选择,因为结果被合并在一起,失去了并行化收益。

这是算法:

const uchar* center = ...;

int t0, t1, val;
t0 = center[0]; t1 = center[1];
val = t0 < t1;
t0 = center[2]; t1 = center[3];
val |= (t0 < t1) << 1;
t0 = center[4]; t1 = center[5];
val |= (t0 < t1) << 2;
t0 = center[6]; t1 = center[7];
val |= (t0 < t1) << 3;
t0 = center[8]; t1 = center[9];
val |= (t0 < t1) << 4;
t0 = center[10]; t1 = center[11];
val |= (t0 < t1) << 5;
t0 = center[12]; t1 = center[13];
val |= (t0 < t1) << 6;
t0 = center[14]; t1 = center[15];
val |= (t0 < t1) << 7;

d[i] = (uchar)val;

这是我在 ARM 汇编中的想法:

VLD2.8 d0, d1 ["center" addr]

假设 8 位字符,第一个操作应该将所有 t0 和 t1 值交替加载到 2 个寄存器中。

VCLT.U8 d2, d0, d1

所有比较的“小于”的单个操作。注意:我读过 VCLT 只能使用 #0 常量作为第二个操作数,因此必须在 >= 中反转。阅读 ARM 文档,我认为每个 8 位值的结果将是“全 1”表示真(11111111)或“全 0”表示假(00000000)。

VSHR.U8 d4, d2, #7

此右移将删除寄存器 8 位“单元”中的 8 个值中的 7 个(主要是删除 7 个)。我使用 d4 是因为下一步将是 q2 中映射的第一个 d 寄存器。

现在问题开始了:移位和 OR。

VSHLL.U8 q2[1], d4[1], 1
VSHLL.U8 q2[2], d4[2], 2
...
VSHLL.U8 q2[7], d4[7], 7

我只能以这种方式想象(如果可以使用 [offsets])进行左移。根据文档,应指定 Q2 而不是 d4。

VORR(.U8) d4[0], d4[1], d4[0]
VORR(.U8) d4[0], d4[2], d4[0]
...
VORR(.U8) d4[0], d4[7], d4[0]

最后一步应该给出结果。

VST1.8 d4[0], [d[i] addr]

结果的简单存储。

这是我第一次使用 ARM NEON,所以可能许多假设可能不正确。帮助我了解可能的错误,并在可能的情况下提出更好的解决方案。

编辑: 这是建议的解决方案之后的最终工作代码:

__asm__ __volatile ("VLD2.8 d0, d1, [%[ordered_center]] \n\t"
"VCGT.U8 d2, d1, d0 \n\t"
"MOV r1, 0x01 \n\t"
"MOV r2, 0x0200 \n\t"
"ORR r2, r2, r1 \n\t"
"MOV r1, 0x10 \n\t"
"MOV r3, 0x2000 \n\t"
"ORR r3, r3, r1 \n\t"
"MOVT r2, 0x0804 \n\t"
"MOVT r3, 0x8040 \n\t"
"VMOV.32 d3[0], r2 \n\t"
"VMOV.32 d3[1], r3 \n\t"
"VAND d0, d2, d3 \n\t"
"VPADDL.U8 d0, d0 \n\t"
"VPADDL.U16 d0, d0 \n\t"
"VPADDL.U32 d0, d0 \n\t"
"VST1.8 d0[0], [%[desc]] \n\t"
:
: [ordered_center] "r" (ordered_center), [desc] "r" (&desc[i])
: "d0", "d1", "d2", "d3", "r1", "r2", "r3");

【问题讨论】:

***.com/questions/11870910/… 不确定为什么在答案中引用它时它没有显示为链接... 【参考方案1】:

比较后,您有一个由 8 个布尔值组成的数组,由 0xff0x00 表示。 SIMD 比较(在任何架构上)产生这些值的原因是使它们对位掩码操作(和/或 NEON 的位选择)有用,因此您可以快速将结果转换为任意值,而无需乘法。

因此,与其将它们减少到10 并移动它们,你会发现用常量0x8040201008040201 来掩盖它们更容易。然后每个通道包含与其在最终结果中的位置相对应的位。您可以将常量预加载到另一个寄存器中(我将使用d3)。

VAND d0, d2, d3

然后,要组合结果,您可以使用VPADD(而不是OR),它将组合相邻的通道对,d0[0] = d0[0] + d0[1]d0[1] = d0[2] + d0[3],等等...由于位模式不重叠没有进位,加法和或一样好。另外,因为输出是输入的一半,我们必须用垃圾填充后半部分。为此,我使用了d0 的第二个副本。

您需要执行 3 次添加才能合并所有列。

VPADD.u8 d0, d0, d0
VPADD.u8 d0, d0, d0
VPADD.u8 d0, d0, d0

现在结果将在d0[0]

如您所见,d0 有另外七个结果的空间; VPADD 操作的一些通道一直在处理垃圾数据。如果您可以一次获取更多数据,并在执行过程中提供额外的工作,这样就不会浪费任何算术,那就更好了。


编辑

假设循环展开四次;结果为d4d5d6d7;前面提到的常量应该被加载到,比如d30d31,然后可以使用一些q寄存器算法:

VAND q0, q2, q15
VAND q1, q3, q15

VPADD.u8 d0, d0, d1
VPADD.u8 d2, d2, d3
VPADD.u8 d0, d0, d2
VPADD.u8 d0, d0, d0 

最终结果在 d0[0..3] 中,或者只是 d0[0] 中的 32 位值。

似乎有很多免费的寄存器可以进一步展开,但我不知道你会在其他计算中用完多少。

【讨论】:

其实这个算法一定是“for (i = 0; i 【参考方案2】:
    加载一个值为 0x8040201008040201 的 d 寄存器 vand 与 vclt 的结果 vpaddl.u8 来自 2) vpaddl.u16 来自 3) vpaddl.u32 来自 4) 存储 5) 中的最低单字节

【讨论】:

当最终结果不能超过一个字节时,为什么要使用vpaddl 这样,其他字节不会被垃圾填充。 OP 现在只想将一个字节存储到内存中,但是如果需要以 32 位值的形式返回,则不需要额外的计算。而且 vpaddl 不会比 vpadd 消耗更多的周期。 其实这个算法一定是“for (i = 0; i 【参考方案3】:

从表达并行性开始明确开始:

int /* bool, whatever ... */ val[8] = 
    center[0] < center[1],
    center[2] < center[3],
    center[4] < center[5],
    center[6] < center[7],
    center[8] < center[9],
    center[10] < center[11],
    center[12] < center[13],
    center[14] < center[15]
;
d[i] = extract_mask(val);

移位相当于“掩码移动”,因为您希望每次比较都产生一个位。

以上16个值的比较可以通过先做一个结构加载(vld2.8)将相邻字节分成两个uint8x8_t,然后并行比较。其结果是 uint8x8_t 在字节中带有 0xff0x00。你想要一个位,在各自的位位置。

那是“面具提取物”;在英特尔 SSE2 上,应该是 MASKMOV,但在 Neon 上,不存在直接等价物;如上所示的三个vpadd(或参见SSE _mm_movemask_epi8 equivalent method for ARM NEON 了解更多信息)是合适的替代品。

【讨论】:

以上是关于ARM NEON 优化中的建议的主要内容,如果未能解决你的问题,请参考以下文章

ARM 中的 NEON 实现

SAD 16*4 的 Arm-neon 优化版本未提供预期增益

使用NEON优化ARM的卷积运算

ARM NEON 图像转换优化

利用ARM NEON intrinsic优化常用数学运算

Eigen NEON 后端是不是经过优化以利用从 ARM A76 开始存在的 2x128b NEON 执行单元?