【问题标题】:Is it normal that each thread in OpenMP does the same amount of work?OpenMP 中的每个线程执行相同数量的工作是否正常?
【发布时间】:2021-06-17 08:25:23
【问题描述】:

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

#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; 
   
   }
    
 }

【问题讨论】:

    标签: c++ multithreading performance parallel-processing openmp


    【解决方案1】:

    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 时需要小心,因为它可能会导致竞态条件。因此,为了将来的使用,您可以使用以下模式来测量 每个 线程所花费的时间,并且仍然避免潜在的 race-conditions

        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;
    

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

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

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

    根据OpenMP 5.1 标准,可以阅读以下关于for 调度子句的内容:

    当 kind 是静态的时,迭代被分成大小的块 chunk_size,并且这些块被分配给团队中的线程 按照线程号的顺序循环方式。 每个块 包含 chunk_size 迭代,除了包含 按顺序最后一次迭代,可能有更少的迭代。 当没有 指定chunk_size,将迭代空间划分为chunks 大小大致相等,最多一块是 分发给每个线程。 块的大小在 这种情况。

    在您的情况下,当使用具有默认块大小的 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 时,默认为 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;
    }
    

    我不明白的另一件事是时间执行 使用静态和动态调度都是一样的。

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

    【讨论】:

      猜你喜欢
      • 2017-05-31
      • 1970-01-01
      • 1970-01-01
      • 2012-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多