首页 > 解决方案 > 打开 MP 瓶颈问题

问题描述

我试图用下面的代码观察一个基本的基于 openMP 的并行性,

#include<stdio.h>
#include<omp.h>
#include<stdlib.h>
#include <time.h>
int main(){
  long i;
  long x[] = {0,0,0,0};
  omp_set_num_threads(4);
  clock_t time=clock();
  #pragma omp parallel for
  for(i=0;i<100000000;i++){
    x[omp_get_thread_num()]++;
  }
  double time_taken = (double)(clock() - time) / CLOCKS_PER_SEC;
  printf("%ld %ld %ld %ld %lf\n",x[0],x[1],x[2],x[3],time_taken);
}

现在,我使用的是四核 i5 处理器。我检查了 4 个不同的线程值。发现以下结果,

Set: omp_set_num_threads(1);
Out: 100000000 0 0 0 0.203921

Set: omp_set_num_threads(2);
Out: 50000000 50000000 0 0 0.826322

Set: omp_set_num_threads(3);
Out: 33333334 33333333 33333333 0 1.448936

Set: omp_set_num_threads(4);
Out: 25000000 25000000 25000000 25000000 1.919655

x数组值是准确的。但是随着线程数量的增加,时间却出人意料地增加了。我无法得到这种现象背后的任何解释/理由。不知何故,omp_get_thread_num()函数本质上是原子的?还是我错过的其他东西?

编译为,gcc -o test test.c -fopenmp

更新

因此,根据已接受答案中的建议,我将代码修改如下,

#include<stdio.h>
#include<omp.h>
#include<stdlib.h>
int main(){
  long i, t_id, fact=1096;
  long x[fact*4];
  x[0]=x[fact]=x[2*fact]=x[3*fact]=0;
  omp_set_num_threads(4);
  double time = omp_get_wtime();
  #pragma omp parallel for private(t_id)
    for(i=0;i<100000000;i++){
      t_id = omp_get_thread_num();
      x[t_id*fact]++;
  }
  double time_taken = omp_get_wtime() - time;
  printf("%ld %ld %ld %ld %lf\n",x[0],x[fact],x[2*fact],x[3*fact],time_taken);
}

现在,结果是可以理解的,

Set: omp_set_num_threads(1)
Out: 100000000 0 0 0 0.250205

Set: omp_set_num_threads(2)
Out: 50000000 50000000 0 0 0.154980

Set: omp_set_num_threads(3)
Out: 33333334 33333333 33333333 0 0.078874

Set: omp_set_num_threads(4)
Out: 25000000 25000000 25000000 25000000 0.061155

因此,它与接受的答案中解释的缓存行大小有关。看看那里得到答案。

标签: c++cmultithreadingpthreadsopenmp

解决方案


正如 gha.st 所指出的,您观察的原因是错误共享和clock函数的属性。

因此x[omp_get_thread_num()],是一种反模式。当然,您可以通过在记忆中增加一个步幅来利用您的新知识。但这也会将特定于硬件的属性(即缓存行大小)编码到您的数据结构中。这会导致令人讨厌的代码难以理解并且仍然具有较差的性能可移植性

惯用的解决方案是使用以下任一方法:

  • 如果您只对聚合感兴趣,请使用reduction子句,即:

    long x = 0;
    #pragma omp parallel for reduction(+:x)
    for(i=0;i<100000000;i++){
        x++;
    }
    // total sum is now in x
    
  • 如果您需要线程中的单个值,只需使用一个private变量,最好是按范围隐式使用。或者,如果您需要从构造外部进行特定初始化,请使用firstprivate.

    #pragma omp parallel
    {
        long local_x = 0; // implicitly private by scope!
        #pragma omp for
        for(i=0;i<100000000;i++) {
            local_x++;
        }
        // can now do something with the the sum of the current thread.
    }
    
  • 如果您需要外部每个线程的结果,您可以使用第二种形式并编写一次结果:

    #pragma omp parallel
    {
        long local_x = 0; // implicitly private by scope!
        #pragma omp for
        for(i=0;i<100000000;i++) {
            local_x++;
        }
        x[omp_get_thread_num()] = local_x;
    }
    

这并不是说您永远不需要在设计数据结构时考虑到错误共享。但它并不像你想象的那么普遍。


推荐阅读