OpenMP 向量化代码运行速度比 O3 优化代码慢

Ath*_*bey 1 c++ gcc simd vectorization openmp

我有一个可重现的样本,如下所示 -

#include <iostream>
#include <chrono>
#include <immintrin.h>
#include <vector>
#include <numeric>



template<typename type>
void AddMatrixOpenMP(type* matA, type* matB, type* result, size_t size){
        for(size_t i=0; i < size * size; i++){
            result[i] = matA[i] + matB[i];
        }
}


int main(){
    size_t size = 8192;

    //std::cout<<sizeof(double) * 8<<std::endl;
    

    auto matA = (float*) aligned_alloc(sizeof(float), size * size * sizeof(float));
    auto matB = (float*) aligned_alloc(sizeof(float), size * size * sizeof(float));
    auto result = (float*) aligned_alloc(sizeof(float), size * size * sizeof(float));


    for(int i = 0; i < size * size; i++){
        *(matA + i) = i;
        *(matB + i) = i;
    }

    auto start = std::chrono::high_resolution_clock::now();

    for(int j=0; j<500; j++){
    
    AddMatrixOpenMP<float>(matA, matB, result, size);
    
}

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

    std::cout<<"Average Time is = "<<duration/500<<std::endl;
    std::cout<<*(result + 100)<<"  "<<*(result + 1343)<<std::endl;

}
Run Code Online (Sandbox Code Playgroud)

我的实验如下 - 我使用函数中#pragma omp for simd循环的指令对代码进行AddMatrixOpenMP计时,然后在没有指令的情况下对其进行计时。我编译代码如下 - g++ -O3 -fopenmp example.cpp

在检查程序集时,两个变体都会生成向量指令,但是当明确指定 OpenMP pragma 时,代码运行速度要慢 3 倍。
我不明白为什么会这样。

编辑 - 我正在运行 GCC 9.3 和 OpenMP 4.5。这是在 Ubuntu 20.04 上的 i7 9750h 6C/12T 上运行的。我确保没有主要进程在后台运行。CPU 频率在两个版本的运行期间或多或少保持不变(从 4.0 到 4.1 的微小变化)

TIA

Pet*_*des 6

非 OpenMP 矢量化器正在通过循环反转击败您的基准测试。
使您的函数__attribute__((noinline, noclone))阻止 GCC 将其内联到重复循环中。对于这样的情况,函数足够大,调用/ret 开销很小,并且常量传播并不重要,这是确保编译器不会将工作提升到循环之外的一种很好的方法。

将来,检查 asm,和/或确保基准时间与迭代次数成线性比例。例如,将 500 增加到 1000 应该会在正常工作的基准测试中提供相同的平均时间,但它不会与-O3. (尽管这里出奇地接近,所以气味测试并不能明确检测到问题!)


在将缺失添加 #pragma omp simd到代码中后,是的,我可以重现这一点。在 i7-6700k Skylake(3.9GHz 带 DDR4-2666)和 GCC 10.2 -O3(不带-march=native-fopenmp)上,我得到 18266,但-O3 -fopenmp平均时间为 39772。

对于 OpenMP 矢量化版本,如果我查看top它运行时的情况,内存使用量 (RSS) 稳定在 771 MiB。(正如预期的那样:两个输入中的 init 代码错误,并且定时区域的第一次迭代写入 ,也为其result触发页面错误。)

但是使用“普通”矢量化器(不是 OpenMP),我看到内存使用量从大约 500 MiB 攀升,直到它在达到最大 770MiB 时退出。

所以看起来gcc -O3在内联之后执行了某种循环反转并击败了基准循环的内存带宽密集型方面,只接触每个数组元素一次。

asm 显示了证据: Godbolt上的GCC 9.3-O3 没有矢量化,它留下一个空的内部循环而不是重复工作。

.L4:                    # outer loop
        movss   xmm0, DWORD PTR [rbx+rdx*4]
        addss   xmm0, DWORD PTR [r13+0+rdx*4]        # one scalar operation
        mov     eax, 500
.L3:                             # do {
        sub     eax, 1                   # empty inner loop after inversion
        jne     .L3              # }while(--i);

        add     rdx, 1
        movss   DWORD PTR [rcx], xmm0
        add     rcx, 4
        cmp     rdx, 67108864
        jne     .L4
Run Code Online (Sandbox Code Playgroud)

这仅比完全完成工作快 2 或 3 倍。可能是因为它没有被矢量化,而且它有效地运行了一个延迟循环,而不是完全优化掉空的内部循环。并且因为现代桌面具有非常好的单线程内存带宽。

将重复计数从 500 增加到 1000 只会将计算出的“平均值”从每次迭代 18266 秒提高到 17821 秒。空循环每个时钟仍然需要 1 次迭代。通常,与重复计数线性缩放是对损坏的基准测试的一个很好的试金石,但这已经足够接近可信了。

定时区域内还有页面错误的开销,但整个过程运行了几秒钟,所以这是次要的。


OpenMP 矢量化版本确实尊重您的基准重复循环。(或者换句话说,无法在此代码中找到可能的巨大优化。)


在基准测试运行时查看内存带宽:

运行intel_gpu_top -l而适当的基准运行显示(OpenMP或用__attribute__((noinline, noclone)))。 IMC是 CPU 芯片上的集成内存控制器,由 IA 内核和 GPU 通过环形总线共享。这就是 GPU 监控程序在这里很有用的原因。

$ intel_gpu_top -l
 Freq MHz      IRQ RC6 Power     IMC MiB/s           RCS/0           BCS/0           VCS/0          VECS/0 
 req  act       /s   %     W     rd     wr       %  se  wa       %  se  wa       %  se  wa       %  se  wa 
   0    0        0  97  0.00  20421   7482    0.00   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   3    4       14  99  0.02  19627   6505    0.47   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   7    7       20  98  0.02  19625   6516    0.67   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
  11   10       22  98  0.03  19632   6516    0.65   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   3    4       13  99  0.02  19609   6505    0.46   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
Run Code Online (Sandbox Code Playgroud)

请注意 ~19.6GB/s 读取/6.5GB/s 写入。读取 ~= 3x 写入,因为它没有将 NT 存储用于输出流。

但是,在-O3击败基准测试和1000重复计数后,我们只看到主内存带宽接近空闲水平。

 Freq MHz      IRQ RC6 Power     IMC MiB/s           RCS/0           BCS/0           VCS/0          VECS/0 
 req  act       /s   %     W     rd     wr       %  se  wa       %  se  wa       %  se  wa       %  se  wa 
...
   8    8       17  99  0.03    365     85    0.62   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   9    9       17  99  0.02    349     90    0.62   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   4    4        5 100  0.01    303     63    0.25   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
   7    7       15 100  0.02    345     69    0.43   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
  10   10       21  99  0.03    350     74    0.64   0   0    0.00   0   0    0.00   0   0    0.00   0   0 
Run Code Online (Sandbox Code Playgroud)

与基准测试根本不运行时 150 到 180 MB/s 读取、35 到 50 MB/s 写入的基线相比。(我有一些程序在运行,即使我没有触摸鼠标/键盘也不会完全休眠。)