编译带有活动霓虹灯标志的代码几乎没有任何改进(甚至恶化)

Compiling code with active neon flags leads to almost no improvement (and even deterioration)

本文关键字:任何改 几乎没有 活动 霓虹灯 标志 代码 编译      更新时间:2023-10-16

我正在努力了解在gcc编译器中使用活动霓虹灯标志编译C++代码可能带来的好处。为此,我制作了一个小程序,可以遍历数组并进行简单的算术运算。

我更改了代码,以便任何人都可以编译和运行它。如果有人愿意执行此测试并分享结果,我将不胜感激:)

编辑:我真的请附近碰巧有Cortex-A9板的人进行这个测试,并检查结果是否相同。我真的很感激。

#include <ctime>
int main()
{
    unsigned long long arraySize = 30000000;
    unsigned short* arrayShort = new unsigned short[arraySize];
    std::clock_t begin;
    for (unsigned long long n = 0; n < arraySize; n++)
    {
        *arrayShort = rand() % 100 + 1;
        arrayShort++;
    }
    arrayShort -= arraySize;
    begin = std::clock();
    for (unsigned long long n = 0; n < arraySize; n++)
    {
        *arrayShort += 10;
        *arrayShort /= 3;
        arrayShort++;
    }
     std::cout << "Time: " << (std::clock() - begin) / (double)(CLOCKS_PER_SEC / 1000) << " ms" << std::endl;
    arrayShort -= arraySize;
    delete[] arrayShort;
    return 0;
}

基本上,我用1到100之间的随机数填充一个30000000大小的数组,然后遍历所有元素,求和10,除以3。我原以为用活动霓虹灯标志编译这段代码会有很大的改进,因为它一次可以进行多个数组操作。

我正在使用带有GCC 4.8.3的Linaro工具链编译此代码,以便在Cortex A9 ARM板上运行。我编译了带有和不带有以下标志的代码:

-O3 -mcpu=cortex-a9 -ftree-vectorize -mfloat-abi=hard -mfpu=neon 

我还复制了用unsigned int、float和double类型的数组运行的代码,这些是以秒为单位的结果:

Array type unsigned short: 
With NEON flags: 0.07s
Without NEON flags: 0.089s
Array type unsigned int: 
With NEON flags: 0.524s
Without NEON flags: 0.529s
Array type float: 
With NEON flags: 0.65s
Without NEON flags: 0.673s
Array type double: 
With NEON flags: 0.955s
Without NEON flags: 0.927s

你可以看到,在大多数情况下,使用霓虹灯标志几乎没有任何改进,甚至在使用替身数组的情况下会导致更糟糕的结果。

我真的觉得我在这里做错了什么,也许你可以帮我解释这些结果。

我不得不用修复你的代码

#include <iostream>
#include <cstdlib>

之后,GCC 5.0自动向量化您的循环,如下所示:

.L7:
    vld1.64 {d16-d17}, [r1:64]
    adds    r4, r4, #1
    vadd.i16    q8, q8, q11
    adc r5, r5, #0
    cmp r3, r5
    add r1, r1, #16
    vmull.u16 q9, d16, d20
    cmpeq   r2, r4
    vmull.u16 q8, d17, d21
    add lr, lr, #16
    vuzp.16 q9, q8
    vshr.u16    q8, q8, #1
    vstr    d16, [lr, #-16]
    vstr    d17, [lr, #-8]
    bhi .L7

所以,是的,编译器可以自动向量化代码,但这有什么好处吗?在我附近的Cortex-A7板上,我看到了以下时间:

g++ ~/foo.cpp -O3
./a.out 
Time: 129.355 ms
g++ ~/foo.cpp -O3 -fno-tree-vectorize
./a.out 
Time: 430.405 ms

它看起来像是您希望的4x矢量化因子(4x16位值)。

在这种情况下,我认为数据和生成的汇编代码不言自明,并驳斥了上面评论中的一些说法。编译器可以,也将执行自动向量化,您可以从中获得的性能是一个有意义的加速。

同样值得注意的是,编译器在评论中击败了一位专业的汇编程序员!

NEON不支持整数除法,因此没有什么可以矢量化。请尝试相乘。

一般情况下是这样,是的。但是,使用霓虹灯除以特定常数的有效序列是存在的,而"3"恰好是这些常数之一!

我的Linaro/UbuntuGCC 4.8.2系统编译器也对这些代码进行了矢量化,生成了与上面非常相似的代码,时间也相似。

我试图使用arm_neon.h内部函数重写这段代码,结果非常令人惊讶。。太多了,以至于我需要一些帮助来解读它们。

这是代码:

#include <ctime>
#include <stdio.h>
#include <cstdlib>
#include <arm_neon.h>
int main()
{
    unsigned long long arraySize = 125000000;
     std::clock_t begin;
    unsigned short* arrayShort = new unsigned short[arraySize];
    for (unsigned long long n = 0; n < arraySize; n++)
    {
        *arrayShort = rand() % 100 + 1;
        arrayShort++;
    }
    arrayShort -= arraySize;
    uint16x8_t vals;
    uint16x8_t constant1 = {10, 10, 10, 10, 10, 10, 10, 10};
    uint16x8_t constant2 = {3, 3, 3, 3, 3, 3, 3, 3};
    begin = std::clock();
    for (unsigned long long n = 0; n < arraySize; n+=8)
    {
        vals = vld1q_u16(arrayShort);
        vals = vaddq_u16(vals, constant1);
        vals = vmulq_u16(vals, constant2);
//      std::cout << vals[0] <<  "   " << vals[1] <<  "   " << vals[2] <<  "   " << vals[3] <<  "   " << vals[4] <<  "   " << vals[5] <<  "   " << vals[6] <<  "   " << vals[7] <<  std::endl;
        arrayShort += 8;
    }
    std::cout << "Time: " << (std::clock() - begin) / (double)(CLOCKS_PER_SEC / 1000) << " ms" << std::endl;
    arrayShort -= arraySize;
    delete[] arrayShort;
    return 0;
}

所以,现在我正在创建一个1.25亿元素长的无符号short数组。然后我一次遍历8个元素,加10,然后乘以3。

在cortex A9板上,该代码的纯C++版本需要270毫秒来处理该阵列,而该NEON代码只需要<strong]20毫秒>

现在,在看到结果之前,我的期望值并不高,但是,我脑海中最好的场景是减少8倍的时间。我无法解释这是如何导致执行时间减少13.5倍的。。我很乐意为您解释这些结果。

很明显,我已经看到了数学运算的结果输出,我可以保证代码是有效的,结果非常一致。

相关文章: