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

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

以下是neon的反汇编版本:

这里是neon的反汇编版本:

                 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速度较慢?


8
@Cody,主题中有一个问题,也许是这个? - Igor Skochinsky
1
C++对于所有整数类型都更快吗?我认为你的汇编代码对于整数类型并不像你希望的那样优化。 - rubenvb
1
问题是为什么 NEON 在 32 位整数类型上速度较慢? - Smalti
@rubenvb 我已经更新了所有类型的工作台报告。 - Smalti
1
对于那些感到困惑的人:NEON是ARM的SIMD扩展,允许128位操作,即一次可以进行4个32位操作。人们期望它在所有情况下都比非SIMD指令更快。http://www.arm.com/products/processors/technologies/neon.php - Mark Ransom
显示剩余4条评论
5个回答

48

在Cortex-A8上,NEON流水线是按顺序执行的,并且具有有限的hit-under-miss(没有重命名),因此您受到内存延迟的限制(因为您使用的内存超过了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时的效果。


6
谢谢回复。我已经使用16个128位寄存器在一次迭代中展开了一个循环。它加速了32位整数。现在的时间是:加法,无符号,C++:180毫秒 加法,无符号,NEON汇编:117毫秒 - Smalti

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 字节。假设您确实得到了未对齐的数组。那么您将在缓存访问时在行之间进行分割,这可能会有额外的惩罚(特别是在缺失时)。
  • 缓存未命中发生在存储后;我不认为 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指令,该指令会加载几个缓存行之外的内容。至于多少行之外:从小开始逐渐增加,直到你不再看到任何好处为止。


这不是由于未对齐的加载 - 这不能解释巨大的差异,尤其是整数也未对齐。Cortex-A8确实具有消歧能力,并且将允许多个加载/存储未命中。根本原因是A8 NEON管道没有命中下的错过,因此需要展开循环。 - John Ripley
2
整数流水线也没有错过。另一方面,NEON可以无序地填充其加载队列(在NEON管道开始之前),这使它能够在L2未命中正在服务时命中L1。整数存储不会是不对齐的,因为malloc不会返回未对齐4字节的内存。因此,没有整数存储会跨越缓存行边界。但这比整数版本慢的根本原因并不是由于缺乏展开,因为整数版本也没有展开。 - Exophase
另一个合理的问题是源和目标是否重叠(特别是如果它们是相同的)。我怀疑NEON是否有任何存储器来加载转发,这将是一个很大的往返,比整数更大。 - Exophase
我认为这与对齐无关。Neon指令的子字符串会自动帮助缓存中的数据对齐。如果我错了,请帮我纠正。 :) - Anoop K. Prabhu

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 却严重滞后。

16位:

情况一样。除了 ARM 的 16 位读取没有那么糟糕。

浮点数:

C++ 版本将编译为 VFP 代码。而在 Coretex A8 上并没有完整的 VFP,只有不进行任何流水线处理的 VFP Lite。

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

顺便说一句,我的完全优化的 NEON 版本比我的完全优化的 C 版本快约 20 倍,比我的完全优化的 ARM 汇编版本快约 8 倍。希望这能让你了解 NEON 的强大之处。

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


你的基准值看起来很有趣!你提到了YUV-RGB转换的数字吗?我得到的速度比以前快7-8倍。20倍的速度相当有趣! - Anoop K. Prabhu
@Anoop:也许我的 C 语言版本不够好? :) 我忘记提到它是 YUV420,平面的 Y 和打包的 UV。在打包的 YUV422 上,也许我就不能得到那种性能提升了。在我的 iPhone4 上,转换 VGA 图像只需不到 1 毫秒。 - Jake 'Alquimista' LEE
我已经学习了几个月的NEON,但从未使用过PLD指令。你的基准测试非常有趣,我会在这里更新我获得的性能提升。顺便说一下,我正在使用Beagleboard。 - Anoop K. Prabhu
当适当放置时,PLD将单独带来约40%的速度提升,假设您正在处理足够大的数据块。只需向前读取即可。在循环开始时,pld [pSrc,#64]最常见。 - Jake 'Alquimista' LEE
感谢您的帮助。期待与您合作。 :) - Anoop K. Prabhu
既然这比OP的更快,我建议编译器需要(需要!)一些工作;-)。您所做的所有更改都应该是从原始、更明确的形式的基本循环分析中产生的候选项。我曾经写过像那样的代码[尤其是do{}while(--n)],因为在80年代和90年代,它有很大的区别。然后有几年的时间,那些不符合“正常”for惯用语法(如OP使用的)的循环会受到惩罚,因为编译器没有太多分析它们。所以我通常不再做那种事情了。Ques现在已经超过4年了,所以现在可能又有所不同了。 - greggo

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]

加入你的循环。

如果更好,请告诉我们...


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 关键字,以获得更好的别名保护。

3
好的,但为什么它不是2或3倍快? - TonyK
由于内存带宽的限制,你在总线传输方面可能已经达到了最快的速度。 - Giovanni Funchal
1
我不是专家,但我认为你需要更复杂的例子才能真正看到优势,无论是在处理数据时所需的工作量(简单的加法并不占用CPU),还是操作数量(数十亿而不是数百万)。我预计会有10-30%的改进,而不是200%。 - Giovanni Funchal
4
对于某些工作负载来说,200%是实际可行的。这里提到的例子只是极端情况:负载和使用分离不佳,以及100%缓存未命中。 - John Ripley
我认为这不是工作量的问题,更多的是一种“你对数据的处理并不需要大量的CPU资源”的问题。 - Giovanni Funchal
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接