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

Sma*_*lti 29 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 ];
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个霓虹灯版本:

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"
    );
}
Run Code Online (Sandbox Code Playgroud)

测试功能:

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 );
}
Run Code Online (Sandbox Code Playgroud)

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

add, unsigned, C++       : 176 ms
add, unsigned, neon asm  : 185 ms // SLOW!!!
Run Code Online (Sandbox Code Playgroud)

我还测试了其他类型:

add, float,    C++       : 571 ms
add, float,    neon asm  : 184 ms // FASTER X3!
Run Code Online (Sandbox Code Playgroud)

问题:为什么使用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
Run Code Online (Sandbox Code Playgroud)

这是霓虹灯的拆解版本:

                 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
Run Code Online (Sandbox Code Playgroud)

以下是所有基准测试:

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
Run Code Online (Sandbox Code Playgroud)

问题:为什么使用32位整数类型的霓虹灯会变慢?

Joh*_*ley 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
...
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢你的答复.我通过在一次迭代中使用16个128位寄存器来展开循环.它加速32位整数.现在时间是:add,unsigned,C++:180 ms add,unsigned,neon asm:117 ms (5认同)

Exo*_*ase 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指令.至于有多少条线路:从小处开始,继续增加它直到你不再看到任何好处.

  • 整数管道也没有命中。另一方面,NEON 可以无序填充其加载队列(在 NEON 管道开始之前),这允许它在 L2 未命中时命中 L1。整数存储不会未对齐,因为 malloc 不会返回未对齐 4 个字节的内存。因此,没有整数存储会跨越缓存行边界。但是这比整数版本慢的根本原因不是由于缺乏展开,因为整数版本也没有展开。 (2认同)

Jak*_*LEE 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);
}
Run Code Online (Sandbox Code Playgroud)

这个版本消耗2个循环/迭代.

此外,您的基准测试结果并不让我感到惊讶.

32位:

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

是的,它非常简单,C++和NEON版本几乎每次都受到管道危害的影响而没有任何真正的机会从双重问题能力中受益.

虽然NEON版本可能会同时处理4个整数,但它也会受到各种危害的影响.就这样.

8位:

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

16位:这里也一样.除了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%的性能提升.


web*_*ker 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
Run Code Online (Sandbox Code Playgroud)

正如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]!
Run Code Online (Sandbox Code Playgroud)

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

您可以尝试添加一个小的

PLD [%[x], 192]
Run Code Online (Sandbox Code Playgroud)

进入你的循环.

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