【问题标题】:OpenMP vectorised code runs way slower than O3 optimized codeOpenMP 矢量化代码的运行速度比 O3 优化代码慢
【发布时间】:2021-08-28 10:15:39
【问题描述】:

我有一个可重复性最低的样本,如下所示 -

#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 时,代码运行速度会慢 3 倍。
我不明白为什么会这样。

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

TIA

【问题讨论】:

  • 如果将#pragma omp for simd 替换为#pragma omp parallel for 会发生什么?
  • @Brannon:请注意,gcc -O2 -fopenmp 默认情况下不进行自动矢量化,用于使用 omp simd 的循环。但是gcc -O3 启用了-ftree-vectorize,它试图向量化任何/所有循环。但是,对于这个简单的纯垂直 SIMD 循环,我预计 OpenMP 向量化器的性能至少与普通向量化器算法一样好。不明白什么可以解释慢 3 倍。
  • 感谢您回复@jjramsey,使用#pragma parallel 确实可以并行化到我所有的12 个线程。有趣的是你买了它,我记得使用#pragma omp parallel for simd,它比 simd 花费了更多时间,尽管我认为这可以归因于线程过载
  • 您在什么硬件上进行了测试? (以及什么操作系统,以防万一)?我假设您控制了 CPU 频率等“热身”效果?您在第一次通过之前不要触摸results,因此它会在定时区域内遭受页面错误,但这对于两个版本应该是相同的,除非您添加另一个定时区域以重用同一程序中的数据。 (如果你这样做了,请参阅Idiomatic way of performance evaluation?
  • 我打算测试它,但它无法编译。您似乎从minimal reproducible example 中遗漏了#includes。修复该问题并添加缺少的#pragma omp simd,是的,在 i7-6700k Skylake(3.9GHz 和 DDR4-2666)上使用 GCC 10.2 -O3(没有 -march=native-fopenmp),我得到 18266,但使用 -O3 -fopenmp我得到平均时间 39772。

标签: c++ gcc openmp vectorization simd


【解决方案1】:

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

将来,请检查 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。 (正如预期的那样:两个输入中的初始化代码错误,定时区域的第一次迭代写入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。 (我有一些程序在运行,即使我没有触摸鼠标/键盘也不会完全休眠。)

【讨论】:

  • 非常感谢您的详细回答,这绝对有帮助
猜你喜欢
  • 2015-05-06
  • 1970-01-01
  • 2017-01-04
  • 2019-04-23
  • 2016-02-11
  • 1970-01-01
  • 2012-05-17
  • 2017-04-10
相关资源
最近更新 更多