使用openmp parallel for loop意外地表现出色

jtr*_*avs 12 parallel-processing multithreading gcc openmp avx

我之前的评论(特别是@Zboson)之后我编辑了我的问题,以提高可读性

我一直采取行动并观察传统观点,即openmp线程的数量应与机器上的超线程数大致匹配,以获得最佳性能.但是,我观察到我的新笔记本电脑采用Intel Core i7 4960HQ,4核 - 8线程的奇怪行为.(请参阅此处的英特尔文档)

这是我的测试代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

当我使用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
Run Code Online (Sandbox Code Playgroud)

编辑:出于好奇,我决定尝试更多的线程.我的操作系统将此限制为2000.奇怪的结果(加速和低线程开销)都说明了一切! 疯狂的线程数

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

正确缩放

Z b*_*son 9

问题可能是由于该clock()功能.它不会在Linux上返回挂起时间.你应该使用这个功能omp_get_wtime().它比时钟更精确,适用于GCC,ICC和MSVC.事实上,即使我没有使用OpenMP,我也会将它用于计时代码.

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

编辑:要考虑其可能导致您的问题的另一件事是expsin功能,您使用的是编译时没有AVX的支持.您的代码是使用AVX支持编译的(实际上是AVX2).如果您使用代码进行编译,您可以使用您的代码从GCC资源管理器中看到这一点.-fopenmp -mavx2 -mfma每当您使用AVX调用没有AVX支持的函数时,您需要将YMM寄存器的上半部分归零或支付大额罚金.您可以使用内在_mm256_zeroupper(VZEROUPPER)执行此操作.Clang为你做了这个,但最后我检查了GCC没有,所以你必须自己做(请参阅这个问题的评论运行任何英特尔AVX功能后,数学函数需要更多周期,这里的答案使用AVX CPU指令:性能不佳"/ 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]);       
}
Run Code Online (Sandbox Code Playgroud)

编辑 一种更简单的测试方法是这样做而不是编译,-march=native不要设置arch(gcc -Ofast -std=c99 -fopenmp -Wa)或只使用SSE2(gcc -Ofast -msse2 -std=c99 -fopenmp -Wa).

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

此选项指示GCC在控制流转出函数之前发出vzeroupper指令,以最小化AVX到SSE转换惩罚以及删除不必要的zeroupper内在函数.