【问题标题】:Unexpectedly good performance with openmp parallel for loopopenmp 并行 for 循环的性能出乎意料的好
【发布时间】:2014-03-24 11:46:16
【问题描述】:

为了更好的可读性,我在之前的 cmets(尤其是 @Zboson)之后编辑了我的问题

我一直遵循并观察到,openmp 线程的数量应与机器上的超线程数量大致匹配以获得最佳性能的传统观念。但是,我在配备 Intel Core i7 4960HQ、4 核 - 8 线程的新笔记本电脑上观察到异常行为。 (见Intel docs here

这是我的测试代码:

#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <omp.h>

int main() {
    const int n = 256*8192*100;
    double *A, *B;
    posix_memalign((void**)&A, 64, n*sizeof(double));
    posix_memalign((void**)&B, 64, n*sizeof(double));
    for (int i = 0; i < n; ++i) {
        A[i] = 0.1;
        B[i] = 0.0;
    }
    double start = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < n; ++i) {
        B[i] = exp(A[i]) + sin(B[i]);
    }
    double end = omp_get_wtime();
    double sum = 0.0;
    for (int i = 0; i < n; ++i) {
        sum += B[i];
    }
    printf("%g %g\n", end - start, sum);
    return 0;
}

当我使用gcc 4.9-4.9-20140209 编译它时,使用命令:gcc -Ofast -march=native -std=c99 -fopenmp -Wa,-q 当我更改OMP_NUM_THREADS 时,我看到了以下性能[这些点是5 次运行的平均值,误差线(几乎不可见)是标准偏差]:

当显示为相对于 OMP_NUM_THREADS=1 的加速时,情节更清晰:

性能或多或少随着线程数单调增加,即使 omp 线程数大大超过核心和超线程数!通常,当使用过多线程时(至少在我以前的经验中),由于线程开销,性能应该会下降。尤其是计算应该受 cpu(或至少是内存)限制,而不是等待 I/O。

更奇怪的是,加速是35倍!

谁能解释一下?

我还用更小的阵列 8192*4 对此进行了测试,并看到了类似的性能扩展。

如果重要的话,我在 Mac OS 10.9 上并且通过运行(在 bash 下)获得的性能数据:

for i in {1..128}; do
    for k in {1..5}; do
        export OMP_NUM_THREADS=$i;
        echo -ne $i $k "";
        ./a.out;
    done;
done > out

编辑:出于好奇,我决定尝试更多数量的线程。我的操作系统将此限制为 2000。奇怪的结果(加速和低线程开销)不言自明!

编辑:我在他们的回答中尝试了@Zboson 的最新建议,即将 VZEROUPPER 放在循环中的每个数学函数之前,它确实解决了缩放问题! (它还将单线程代码从 22 秒发送到 2 秒!):

【问题讨论】:

  • 这可能是 OpenMP 分配线程的方式,您是否出于好奇尝试了 3 个线程?可能是当从 1 移动到 2 时,它会将两个线程分配给一个 ACTUAL 核心,但是因为您确实试图在该单一核心中利用相同的资源,所以它真的没有帮助!移动到 4 时,您实际上是在使用 2 个实际核心(也许)。另外,如果你使用 8 个线程会发生什么,所以我们可以看到当我们从(希望)超线程情况转移到全核心情况 + 超线程时会发生什么?
  • @trumpetlicks 我添加了你想要的时间。
  • 另外,如果你要多次运行每个(除了单个案例),那么计时结果是多少。我认为 OpenMP 和操作系统随机分配给核心 #(或者在您的情况下,它可能分配给 HT 或实际核心)。
  • 您在哪里更改编号。使用的线程数?
  • @Neuron 使用 OMP_NUM_THREADS 环境变量

标签: multithreading gcc parallel-processing openmp avx


【解决方案1】:

问题可能是由clock() 函数引起的。它不会返回 Linux 上的挂钟时间。您应该使用函数omp_get_wtime()。它比时钟更准确,适用于 GCC、ICC 和 MSVC。事实上,即使我不使用 OpenMP,我也会将它用于计时代码。

我在这里用它测试了你的代码 http://coliru.stacked-crooked.com/a/26f4e8c9fdae5cc2

编辑:要考虑的另一件事可能导致您的问题是您正在使用的 expsin 函数是在没有 AVX 支持的情况下编译的。您的代码是使用 AVX 支持编译的(实际上是 AVX2)。如果您使用 -fopenmp -mavx2 -mfma 编译,您可以从带有代码的 GCC explorer 中看到这一点 每当您从带有 AVX 的代码中调用不支持 AVX 的函数时,您需要将 YMM 寄存器的上部归零或支付巨额罚款。您可以使用内在的_mm256_zeroupper (VZEROUPPER) 来做到这一点。 Clang 为您执行此操作,但最后我检查了 GCC 没有,因此您必须自己执行此操作(请参阅此问题的 cmets Math functions takes more cycles after running any intel AVX function 以及此处的答案 Using AVX CPU instructions: Poor performance without "/arch:AVX")。因此,由于没有调用 VZEROUPPER,每次迭代都会有很大的延迟。我不确定为什么这对多线程很重要,但如果 GCC 每次启动一个新线程时都这样做,那么它可以帮助解释你所看到的。

#include <immintrin.h>

#pragma omp parallel for
for (int i = 0; i < n; ++i) {
    _mm256_zeroupper();
    B[i] = sin(B[i]);
    _mm256_zeroupper();
    B[i] += exp(A[i]);       
}

编辑一个更简单的测试方法是不要设置拱门 (gcc -Ofast -std=c99 -fopenmp -Wa) 或只使用 SSE2 (gcc -Ofast -msse2 -std=c99 -fopenmp -Wa),而不是使用 -march=native 编译。

编辑 GCC 4.8 有一个选项 -mvzeroupper 可能是最方便的解决方案。

此选项指示 GCC 在将控制流转移出函数之前发出 vzeroupper 指令,以最大限度地减少 AVX 到 SSE 的转换损失,并删除不必要的 zeroupper 内在函数。

【讨论】:

  • 按你的时间计算。热身只是确保您忘记考虑 OpenMP 的成本,这是误导性的。成本就是成本,忍受它。
  • 我可以说不热身是一种误导。如果您要多次使用您的功能,而您只报告盯着冷的时间,那么这是一种误导。最好报告最坏情况和最佳情况时间。这样更准确。
  • @JoelFalcou,举个例子。我使用 OpenMP 渲染 Mandelbrot 设置每秒几帧。由于 OpenMP 预热,第一帧总是最慢的。这不仅仅是缓存的问题,因为我可以更改我渲染的内容(缩放、翻译)并返回到初始设置,而且它只是第一帧这么慢。如果我只报告第一帧的时间,那将是误导。在这种情况下,最佳情况时间更准确。
  • 通常最好的方法是运行 大量 数量的样本,然后取中间值或第一个十分位值。无论如何,Mandelbrodt 中也不存在缓存问题,因为您只将 valeu 存储到目标缓冲区。所以是的,第一帧很慢,因为线程启动+缓存很冷。中值时间更适合这一点,因为它消除了所有异常值,而不仅仅是第一个。
  • @Zboson 我只想并行化一个循环,因为我正在比较许多不同语言/系统上的相同内核计算。出于同样的原因,我想包括所有 openmp 开销。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-19
  • 2020-04-16
相关资源
最近更新 更多