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

OpenMP vectorised code runs way slower than O3 optimized code

我有一个最小可重现的样本如下 -

#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;

}

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

在检查程序集时,两种变体都生成矢量指令,但是当明确指定 OpenMP pragma 时,代码 运行s 慢了 3 倍。
我无法理解为什么会这样。

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

TIA

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

并且在未来,检查 asm,and/or 确保基准时间与迭代次数呈线性关系。例如将 500 增加到 1000 应该会在正常工作的基准测试中给出相同的平均时间,但它不会与 -O3 一起使用。 (虽然这里出奇地接近,所以气味测试并不能明确地检测到问题!)


在代码中添加缺少的 #pragma omp simd 之后,是的,我可以重现它。在带有 GCC 10.2 -O3(没有 -march=native-fopenmp)的 i7-6700k Skylake(3.9GHz,带 DDR4-2666)上,我得到 18266,但在 -O3 -fopenmp 的情况下,我得到平均时间 39772。

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

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

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

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

.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

这只比完全完成工作快 2 或 3 倍。可能是因为它没有矢量化,它实际上是 运行 一个延迟循环,而不是完全优化掉空的内部循环。并且因为现代台式机具有非常好的单线程内存带宽。

将重复计数从 500 增加到 1000 只会将计算出的“平均值”从每迭代 18266 提高到 17821 us。空循环仍需要每个时钟进行 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 

注意~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 

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