1

出于学术目的,我想尝试为以下算法编写一个 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");
4

3 回答 3

1

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

因此,与其将它们简化为1or0并移动它们,您会发现用常量来掩盖它们更容易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运营渠道一直在处理垃圾数据。如果您可以一次获取更多数据,并在进行时提供额外的工作,这样就不会浪费任何算术,那就更好了。


编辑

假设循环展开四次;结果为d4, d5, d6, 和d7; 前面提到的常量应该被加载到,比如说,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 位值。

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

于 2013-07-05T14:24:38.677 回答
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);

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

上述十六个值的比较可以通过先执行结构加载(vld2.8)将相邻字节一分为二uint8x8_t,然后进行并行比较来完成。其结果是字节中的uint8x8_t一个0xff或一个。0x00您需要在各自的位位置中的每个位。

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

于 2013-07-25T11:36:25.727 回答
0
  1. 加载值为 0x8040201008040201 的广告寄存器
  2. vand 与 vclt 的结果
  3. vpaddl.u8 来自 2)
  4. vpaddl.u16 来自 3)
  5. vpaddl.u32 来自 4)
  6. 存储 5) 中的最低单字节
于 2013-07-05T14:47:31.057 回答