31

这是一个 C++ 代码:

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    for ( register int i = 0; i < ARR_SIZE_TEST; ++i )
    {
        x[ i ] = x[ i ] + y[ i ];
    }
}

这是霓虹灯版本:

void neon_assm_tst_add( unsigned* x, unsigned* y )
{
    register unsigned i = ARR_SIZE_TEST >> 2;

    __asm__ __volatile__
    (
        ".loop1:                            \n\t"

        "vld1.32   {q0}, [%[x]]             \n\t"
        "vld1.32   {q1}, [%[y]]!            \n\t"

        "vadd.i32  q0 ,q0, q1               \n\t"
        "vst1.32   {q0}, [%[x]]!            \n\t"

        "subs     %[i], %[i], $1            \n\t"
        "bne      .loop1                    \n\t"

        : [x]"+r"(x), [y]"+r"(y), [i]"+r"(i)
        :
        : "memory"
    );
}

测试功能:

void bench_simple_types_test( )
{
    unsigned* a = new unsigned [ ARR_SIZE_TEST ];
    unsigned* b = new unsigned [ ARR_SIZE_TEST ];

    neon_tst_add( a, b );
    neon_assm_tst_add( a, b );
}

我已经测试了这两种变体,这是一份报告:

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 185 ms // SLOW!!!

我还测试了其他类型:

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms // FASTER X3!

问题:为什么 32 位整数类型的 neon 速度较慢?

我为 Android NDK 使用了最新版本的 GCC。NEON 优化标志已打开。这是一个反汇编的 C++ 版本:

                 MOVS            R3, #0
                 PUSH            {R4}

 loc_8
                 LDR             R4, [R0,R3]
                 LDR             R2, [R1,R3]
                 ADDS            R2, R4, R2
                 STR             R2, [R0,R3]
                 ADDS            R3, #4
                 CMP.W           R3, #0x2000000
                 BNE             loc_8
                 POP             {R4}
                 BX              LR

这是霓虹灯的反汇编版本:

                 MOV.W           R3, #0x200000
.loop1
                 VLD1.32         {D0-D1}, [R0]
                 VLD1.32         {D2-D3}, [R1]!
                 VADD.I32        Q0, Q0, Q1
                 VST1.32         {D0-D1}, [R0]!
                 SUBS            R3, #1
                 BNE             .loop1
                 BX              LR

以下是所有基准测试:

add, char,     C++       : 83  ms
add, char,     neon asm  : 46  ms FASTER x2

add, short,    C++       : 114 ms
add, short,    neon asm  : 92  ms FASTER x1.25

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 184 ms SLOWER!!!

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms FASTER x3

add, double,   C++       : 533 ms
add, double,   neon asm  : 420 ms FASTER x1.25

问题:为什么 32 位整数类型的 neon 速度较慢?

4

5 回答 5

48

Cortex-A8 上的 NEON 管道是按顺序执行的,并且命中率有限(无重命名),因此您受到内存延迟的限制(因为您使用的缓存大小超过了 L1/L2 缓存大小)。您的代码直接依赖于从内存加载的值,因此它会不断地等待内存。这可以解释为什么 NEON 代码比非 NEON 代码稍慢(一点点)。

您需要展开组装循环并增加负载和使用之间的距离,例如:

vld1.32   {q0}, [%[x]]!
vld1.32   {q1}, [%[y]]!
vld1.32   {q2}, [%[x]]!
vld1.32   {q3}, [%[y]]!
vadd.i32  q0 ,q0, q1
vadd.i32  q2 ,q2, q3
...

有很多霓虹灯寄存器,所以你可以展开很多。整数代码也会遇到同样的问题,但程度较轻,因为 A8 整数具有更好的命中率而不是停滞。对于与 L1/L2 缓存相比如此大的基准测试,瓶颈将是内存带宽/延迟。您可能还希望以较小的大小 (4KB..256KB) 运行基准测试,以查看数据完全缓存在 L1 和/或 L2 中时的效果。

于 2011-04-20T17:07:39.660 回答
17

尽管在这种情况下您受到主内存延迟的限制,但 NEON 版本会比 ASM 版本慢并不是很明显。

在此处使用循环计算器:

http://pulsar.webshaker.net/ccc/result.php?lng=en

在缓存未命中惩罚之前,您的代码应该需要 7 个周期。它比您预期的要慢,因为您使用的是未对齐的负载以及添加和存储之间的延迟。

同时,编译器生成的循环需要 6 个周期(通常也没有很好地安排或优化)。但它的工作量是原来的四分之一。

脚本中的循环计数可能并不完美,但我没有看到任何看起来明显错误的地方,所以我认为它们至少会接近。如果您最大化获取带宽(如果循环不是 64 位对齐的),则可能会在分支上花费一个额外的周期,但在这种情况下,有很多停顿可以隐藏这一点。

答案不是 Cortex-A8 上的整数有更多隐藏延迟的机会。事实上,由于 NEON 的交错管道和问题队列,它通常具有较少的内容。当然,这仅在 Cortex-A8 上是正确的 - 在 Cortex-A9 上,情况可能会完全相反(NEON 是按顺序调度并与整数并行,而整数具有无序功能)。既然你标记了这个 Cortex-A8,我假设这就是你正在使用的。

这需要更多的调查。以下是为什么会发生这种情况的一些想法:

  • 您没有在数组上指定任何类型的对齐方式,虽然我希望 new 与 8 字节对齐,但它可能不会与 16 字节对齐。假设您确实得到了不是 16 字节对齐的数组。然后,您将在缓存访问的行之间进行拆分,这可能会产生额外的损失(尤其是未命中)
  • 缓存未命中发生在存储之后;我不相信 Cortex-A8 有任何内存消歧,因此必须假设加载可能来自与存储相同的行,因此需要写缓冲区在 L2 丢失加载发生之前耗尽。因为 NEON 负载(在整数管道中启动)和存储(在 NEON 管道末端启动)之间的管道距离比整数负载大得多,所以可能会有更长的停顿时间。
  • 因为每次访问加载 16 个字节而不是 4 个字节,所以关键字大小更大,因此来自主内存的关键字优先行填充的有效延迟会更高(L2 到 L1 应该在 128 位总线上,所以不应该有同样的问题)

您问在这种情况下 NEON 有什么好处 - 实际上,NEON 特别适用于您在内存中进行流式传输的情况。诀窍是您需要使用预加载来尽可能隐藏主内存延迟。预加载会提前将内存放入 L2(而不是 L1)缓存。在这里 NEON 比整数有很大的优势,因为它可以隐藏很多 L2 缓存延迟,这是由于它交错的管道和问题队列,还因为它有一个直接的路径。我希望你看到有效的 L2 延迟低至 0-6 个周期,如果你有更少的依赖并且不会耗尽负载队列,那么你会看到更少的延迟,而在整数上,你可能会遇到无法避免的大约 16 个周期(可能取决于 Cortex-A8)。

因此,我建议您将数组与缓存行大小(64 字节)对齐,展开循环以一次至少执行一个缓存行,使用对齐的加载/存储(在地址后放置 :128)并添加pld 指令加载几个缓存行。至于有多少行:从小处开始并不断增加它,直到您不再看到任何好处。

于 2011-05-30T17:36:49.443 回答
15

您的 C++ 代码也没有优化。

#define ARR_SIZE_TEST ( 8 * 1024 * 1024 )

void cpp_tst_add( unsigned* x, unsigned* y )
{
    unsigned int i = ARR_SIZE_TEST;
    do
    {
        *x++ += *y++;
    } (while --i);
}

此版本消耗更少的 2 个周期/迭代。

此外,你的基准测试结果一点也不让我吃惊。

32位:

这个功能对于NEON来说太简单了。没有足够的算术运算为优化留下任何空间。

是的,它非常简单,以至于 C++ 和 NEON 版本几乎每次都遭受管道危害,而没有任何真正的机会从双重问题功能中受益。

虽然 NEON 版本可能会受益于一次处理 4 个整数,但它也会因各种危险而遭受更多的损失。就这样。

8位:

ARM 从内存中读取每个字节的速度非常慢。这意味着,虽然 NEON 显示出与 32 位相同的特性,但 ARM 严重滞后。

16bit :这里也一样。除了 ARM 的 16 位读取还不错。

float : C++ 版本将编译成 VFP 代码。Coretex A8 上没有完整的 VFP,但 VFP lite 不会流水线任何糟糕的东西。

并不是说 NEON 在处理 32 位时表现得很奇怪。只有 ARM 符合理想条件。由于其简单性,您的函数非常不适合进行基准测试。尝试一些更复杂的东西,比如 YUV-RGB 转换:

仅供参考,我完全优化的 NEON 版本的运行速度大约是我完全优化的 C 版本的 20 倍,是我完全优化的 ARM 汇编版本的 8 倍。我希望这能让您了解 NEON 的强大功能。

最后但同样重要的是,ARM 指令 PLD 是 NEON 最好的朋友。放置得当,它会带来至少 40% 的性能提升。

于 2011-11-02T13:02:24.257 回答
5

您可以尝试一些修改来改进代码。

如果可以: - 使用第三个缓冲区来存储结果。- 尝试在 8 个字节上对齐数据。

代码应该是这样的(对不起,我不知道 gcc 内联语法)

.loop1:
 vld1.32   {q0}, [%[x]:128]!
 vld1.32   {q1}, [%[y]:128]!
 vadd.i32  q0 ,q0, q1
 vst1.32   {q0}, [%[z]:128]!
 subs     %[i], %[i], $1
bne      .loop1

正如 Exophase 所说,您有一些管道延迟。可能你可以试试

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

sub     %[i], %[i], $1

.loop1:
vadd.i32  q2 ,q0, q1

vld1.32   {q0}, [%[x]:128]
vld1.32   {q1}, [%[y]:128]!

vst1.32   {q2}, [%[z]:128]!
subs     %[i], %[i], $1
bne      .loop1

vadd.i32  q2 ,q0, q1
vst1.32   {q2}, [%[z]:128]!

最后,很明显你会使内存带宽饱和

您可以尝试添加一个小

PLD [%[x], 192]

进入你的循环。

告诉我们是否更好...

于 2011-06-07T07:19:40.370 回答
0

8 毫秒的差异是如此之小,以至于您可能正在测量缓存或管道的工件。

编辑:您是否尝试过与类似这样的类型进行比较,例如 float 和 short 等?我希望编译器能够更好地优化它并缩小差距。同样在您的测试中,您首先执行 C++ 版本,然后执行 ASM 版本,这可能会对性能产生影响,因此为了更公平,我会编写两个不同的程序。

for ( register int i = 0; i < ARR_SIZE_TEST/4; ++i )
{
    x[ i ] = x[ i ] + y[ i ];
    x[ i+1 ] = x[ i+1 ] + y[ i+1 ];
    x[ i+2 ] = x[ i+2 ] + y[ i+2 ];
    x[ i+3 ] = x[ i+3 ] + y[ i+3 ];
}

最后一件事,在你的函数签名中,你使用unsigned*而不是unsigned[]. 后者是首选,因为编译器假定数组不重叠并且允许重新排序访问。尝试使用restrict关键字也可以更好地防止混叠。

于 2011-04-20T16:52:27.267 回答