ARM NEON优化的建议

时间:2013-07-05 14:08:17

标签: optimization assembly arm neon

出于学术目的,我想尝试编写以下算法的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位字符,第一个操作应该在2个寄存器中交替加载所有t0和t1值。

VCLT.U8 d2, d0, d1

所有比较的“少于”的单个操作。注意:我已经读过VCLT只能用#0常量作为第二个操作数,因此必须在&gt; =中反转。阅读ARM文档我认为每个8位值的结果对于true(11111111)将是“全1”,对于false(00000000)将是“全0”。

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

我只能想象这种方式(如果可以使用[偏移])左移。根据文档,应该指定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");

3 个答案:

答案 0 :(得分:1)

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

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

VAND d0, d2, d3

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

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

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位值。

似乎有很多寄存器可以自由地展开它,但我不知道有多少寄存器会用于其他计算。

答案 1 :(得分:0)

  1. 加载值为0x8040201008040201
  2. 的d寄存器
  3. vand与vclt的结果
  4. vpaddl.u8来自2)
  5. vpaddl.u16 from 3)
  6. vpaddl.u32 from 4)
  7. 存储5)中最低的单字节

答案 2 :(得分:0)

首先明确表达并行

开头。

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,但在霓虹灯上,没有直接的等价物;上面显示的三个vpadd(或参见SSE _mm_movemask_epi8 equivalent method for ARM NEON了解更多信息)是合适的替代品。