为什么ARM NEON不比普通的C ++快?

时间:2011-04-20 12:07:41

标签: c++ arm neon cortex-a8

这是一个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位整数类型的氖速度较慢?

我使用了最新版本的GCC for Android NDK。打开了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位整数类型的氖速度较慢?

5 个答案:

答案 0 :(得分:44)

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中时的效果。

答案 1 :(得分: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个周期,如果你有较少的依赖关系并且不会耗尽加载队列,那么你会看到有效的L2延迟,而在整数上你可能会遇到一个你无法避免的好的~16个周期(可能虽然取决于Cortex-A8。

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

答案 2 :(得分:12)

您的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位读取并不是那么糟糕。

浮动: C ++版本将编译为VFP代码。并且Coretex A8上没有完整的VFP,但VFP lite并没有管理任何糟糕的东西。

并不是说NEON表现得很奇怪,处理32位。它只是满足理想条件的ARM。 由于其简单性,您的功能非常不适合基准测试目的。尝试更复杂的东西,如YUV-RGB转换:

仅供参考,我完全优化的NEON版本的运行速度大约是我完全优化的C版本的20倍,是我完全优化的ARM装配版本的8倍。 我希望能让你知道NEON有多强大。

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

答案 3 :(得分: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]

进入你的循环。

告诉我们它是否更好......

答案 4 :(得分:2)

8ms的差异 SO 很小,您可能正在测量缓存或管道的工件。

编辑:您是否尝试过类似浮动和短片等类型的比较?我希望编译器能够更好地优化它并缩小差距。同样在你的测试中你先做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关键字来更好地防止别名。