繁体   English   中英

OpenMP 中的每个线程执行相同数量的工作是否正常?

[英]Is it normal that each thread in OpenMP does the same amount of work?

对于以下代码,我计算了每个线程的执行时间,但奇怪的是,在我使用 static 或动态调度进行的所有运行中,每个线程都有几乎准确的时间调用。 这是 OpenMP 中预期的吗? 我们是否曾经遇到过一个或多个线程执行更多工作的情况? 我不明白的另一件事是使用 static 和动态计划的时间执行是相同的。 恐怕我计算时间的方式有误。

#include <iostream>
#include <vector>
#include <random>
#include <cmath>
#include <omp.h>
#include <fstream>
#include <cfloat>
#include <chrono>
using namespace std;
using namespace chrono; 
int main()
{
    const int N = 100000;
    ofstream result{"Result.txt"};
    vector<vector<double>> c;
    default_random_engine g(0);
    uniform_real_distribution<double> d(0.0f, nextafter(1.0f, DBL_MAX));
    c.reserve(N);

    for (int i = 0; i < N; i++) {
        const unsigned size = pow(10, i % 4);
        vector<double> a;
        a.reserve(size);

        for (int j = 0; j < size; j++) {
            const double number = d(g);
            a.push_back(number);
        }

        c.push_back(std::move(a));
    }

    double sum = 0.0;
    vector<double> b(N);
    int total_threads=4; 
    double time_taken_by_threads[total_threads];
    auto t1= high_resolution_clock::now();
    
    #pragma omp parallel num_threads(4) firstprivate(N) shared(b,c,sum)
    
    {
        int threadID = omp_get_thread_num();
        double start = omp_get_wtime();
     
    
        #pragma omp for reduction(+:sum) schedule(dynamic)
        for (int i = 0; i < N ; i++) {
            double sumLocal = 0.0;

            for (int j = 0; j < c[i].size();j++) {
                sumLocal += pow(c[i][j], 2);
            }

            const double n = sqrt(sumLocal);
            b[i] = n;

            sum += sumLocal;
        }
        
      
        double end = omp_get_wtime();
       time_taken_by_threads[threadID] = end - start;
    }
      
    
    auto t2=high_resolution_clock::now();
    
    auto diff=duration_cast<milliseconds>(t2-t1);
    
    cout<<"The total job has been taken : "<<diff.count()<<endl; 
    


   for(int i=0; i<total_threads ; i++){
   
   cout<<" Thread work "<<  time_taken_by_threads[i]<<endl; 
   
   }
    
 }

TL;DR您在#pragma omp for reduction(+:sum)


恐怕我计算时间的方式有误。

事实上,它总是会给出类似的结果,因为#pragma omp for

    double start = omp_get_wtime();
    #pragma omp for reduction(+:sum) schedule(dynamic)
    for (int i = 0; i < N ; i++) {
        // ....
    }
   // <--- threads will wait here for one another.
   double end = omp_get_wtime();
   time_taken_by_threads[threadID] = end - start;

在循环之后引入了一个隐式barrier 所以首先完成的线程仍然会等待那些没有完成的线程。 要删除该隐式障碍,您可以使用nowait子句:

#pragma omp for reduction(+:sum) schedule(dynamic) nowait

尽管在这段代码中这不是问题,但在移除隐式barrier时需要小心,因为它可能会导致竞争条件 因此,为了将来的使用,您可以使用以下模式来测量每个线程所花费的时间,并且仍然避免潜在的竞争条件

    double start = omp_get_wtime();
    // The parallel loop with nowait
    double end = omp_get_wtime();
    #pragma omp barrier
    time_taken_by_threads[threadID] = end - start;

然而,即使进行了这种更改,每个线程所花费的时间也应该大致相同。 我将在下面解释为什么会这样。

对于以下代码,我计算了每个线程的执行时间,但奇怪的是,在我使用 static 或动态调度进行的所有运行中,每个线程都有几乎准确的时间调用。 这是 OpenMP 中预期的吗?

可以预期,当使用static调度时,OpenMP 会尝试在线程之间尽可能均匀地划分循环迭代次数。

OpenMP 5.1标准中,可以阅读以下关于for schedule 子句的内容:

当 kind 为 static 时,迭代被分成大小为 chunk_size 的块,并且这些块按照线程号的顺序以循环方式分配给团队中的线程。 每个块都包含 chunk_size 迭代,除了包含顺序最后一次迭代的块,它可能有更少的迭代。 当没有指定chunk_size时,迭代空间被分成大小近似相等的chunk,每个线程最多分配一个chunk。 在这种情况下,未指定块的大小。

在您的情况下,当使用具有默认块大小的static分布时,4 个线程中的每一个都将计算 25000 次迭代(100000/4)。

如果我们分析并行循环:

#pragma omp for reduction(+:sum) schedule(static)
for (int i = 0; i < N ; i++) {
    double sumLocal = 0.0;

    for (int j = 0; j < c[i].size();j++) {
        sumLocal += pow(c[i][j], 2);
    }

    const double n = sqrt(sumLocal);
    b[i] = n;

    sum += sumLocal;
}

我们可以看到每次迭代执行相同数量的计算,并且计算主要受 CPU 限制,因此可以预期每个线程将花费大致相同的时间。

关于 OpenMP 5.1 标准中的动态时间表,可以阅读:

当 kind 是动态的时,迭代以块的形式分发到团队中的线程。 每个线程执行一个迭代块,然后请求另一个块,直到没有块需要分发。 每个块都包含 chunk_size 迭代,除了包含顺序最后一次迭代的块,它可能有更少的迭代。 当没有指定 chunk_size 时,默认为 1。

因此,由于默认情况下块大小为 1,并且我们已经知道循环的迭代将花费大致相同的时间,因此可以预期线程也将花费相同的时间。

我们是否曾经遇到过一个或多个线程执行更多工作的情况?

当然,您只需要创建一个导致负载不平衡的情况,例如:

#pragma omp parallel for schedule(static)
  for(int i=0; i<N; i++){
      for(int k=0; k<i; k++){
          // some computation  
       }
   }

如果你仔细看,你可以看到内循环的工作以三角形(N = SIZE)的形状增长:

 *k/i 0 1 2 3 4 5 ... N-1
 *  0 - x x x x x ... x 
 *  1 - - x x x x ... x 
 *  2 - - - x x x ... x
 *  3 - - - - x x ... x
 *  4 - - - - - x ... x
 *  5 - - - - - - ... x
 *  . - - - - - - ... x
 *  . - - - - - - ... x 
 *N-1 - - - - - - ... -    
 *  N - - - - - - ... - 

因此,对于 4 个线程和N使得N % 4 = 0 ,线程 1 将分配循环的前N/4次迭代,线程 2 分配下一个N/4等等。 因此,线程 1 用较少的最内层循环迭代计算最外层循环迭代,这会导致负载不平衡,并最终导致线程在完成并行工作所需的时间之间存在较大差异。

您可以在代码中模拟该场景,如下所示:

#pragma omp for reduction(+:sum) schedule(static) nowait
for (int i = 0; i < N ; i++) {
    double sumLocal = 0.0;

    for (int j = i; j < c[i].size();j++) {
        sumLocal += pow(c[i][j], 2);
    }
    const double n = sqrt(sumLocal);
    b[i] = n;

    sum += sumLocal;
}

我不明白的另一件事是使用 static 和动态计划的时间执行是相同的。

正如我们已经解释的那样,考虑到分配给每个线程的并行任务的性质,这是可以预料的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM