OpenMP学习 第五章 并行化循环

第五章 并行化循环


共享工作循环构造

循环级并行: 将一定规模的涉及循环的问题转换为SPMD模式的并行.

共享工作循环构造: 在一个线程组中拆分循环迭代的指令.

  • 使用共享工作循环构造的结构:
#pragma omp for
    //for loop

在实际使用过程中,下面的模式是常常可见的:一个用来创建线程组的构造,一个用来分割线程之间循环迭代的构造.

  • 单独式并行共享工作循环构造:
#pragma omp parallel
{
    #pragma omp for
        //for loop
}

为方便,二者可以结合:

  • 组合式并行共享工作循环构造:
#pragma omp parallel for
    //for loop

归约

循环依赖性: 在任何给定循环的迭代中计算的值都依赖于前面迭代产生的值.使用共享工作循环构造无法解决这种依赖性.

考虑下面这样的情景:

double ave = 0.0;
double A[N];

init(A,N);
for(int i=0;i<N;i++)
    ave += A[i];
ave = ave/N

归约: 存在循环依赖性的形似SPMD结构的情景.

为了解决归约情况中的循环依赖性,提供一个reduction字句对其进行处理.

  • 使用reduction字句解决循环依赖性:
#pragma omp parallel for reduction(op:list)

其中list为以逗号间隔的变量列表,op及其默认初始值见下表:

运算符 初始值
+ 0
* 1
- 0
min 最大正数
max 最大负数

对于列表中的每一个变量(归约变量),系统将为每个线程创建一个同名的私有变量,每个线程将其是有变量的副本初始化为字句中的指示符(op)的隐含初始值.

在线程完成构造并且退出栅栏之前,利用归约字句中的op将每个线程的归约变量的本地副本合并在一起产生最终的归约值,然后利用op将其与原始变量合并,产生最终结果.

下面是一个实例,其通过reduction字句进行归约,解决了n阶乘的计算问题:

#include <iostream>
#include <omp.h>
import <format>;

int main()
{
    long long total = 1;
    int n;
    std::cin >> n;

    double begin_time;
    begin_time = omp_get_wtime();

    omp_set_num_threads(10);
    #pragma omp parallel for reduction(*:total)
    for (int i = 1; i <= n; i++) {
        total *= i;
        int id = omp_get_thread_num();

        #pragma omp critical
            std::cout << std::format("the {} thread now over.i is {}",
                id,
                i
            ) << std::endl;
    }

    double run_time;
    run_time = omp_get_wtime() - begin_time;
    std::cout << std::format("the total is {},run_time is {:.15f}",
        total,
        run_time
    ) << std::endl;

    return 0;
}

需要注意的,由于浮点运算不算是严格意义上的结合性运算,因而其归约结果可能会因为程序的不同运行而不同.

循环调度

在共享工作循环构造的基础上,为了实现循环调度,添加了一个schedule子句

  • 使用schedule子句进行静态调度:
#pragma omp for schedule(static[,chunk]) 
  • 使用schedule子句进行动态调度:
#pragma omp for schedule(dynamic[,chunk])

可选的分块(chunk)大小定义了构成调度的基本单元的循环迭代次数.分块大小可以是一个在编译时已知的值,也可以是一个在运行时计算的含有共享变量的整数表达式.

静态调度: 在共享工作循环构造的基础上,在"编译"时将循环迭代映射到线程上.
当没有提供分块大小时,编译器会将循环迭代分解为与线程总数相等数量的分块,
当提供了分块大小,OpenMP将把循环分成连续的迭代分块,以轮询调度的方式分配给每个线程.

通常而言,静态调度中,最佳分块大小需要通过一系列的尝试才能得到.

动态调度: 当循环迭代的运行时间大致相同时,静态调度可以很好地适应这种情况.当循环迭代具有可预测的运行时间时,它也很有用.

使用OpenMP的挑战之一是均衡各线程的负载.dynamic调度提供了自动负载均衡,然而,其运行时调度开销比使用static调度观察到的开销要高得多.

需要注意,schedule(static,1)实际上是循环迭代的周期分配,schedule(static)则为块状分配.

通常而言,共享工作构造在构造结束时有一个隐式栅栏,如果可以确定在一个共享工作构造的结尾不需要栅栏,那么需要一种方法来禁用它.

对于共享工作循环构造来说,可以通过nowait子句实现.

#pragma ompr for nowait

事实上,在某些合适的情景(可以禁用隐式栅栏)中对共享工作构造使用nowait子句可以为其带来不小的性能提升,下面通过一个实验说明:

#include <iostream>
#include <vector>
#include <omp.h>
import <format>;

#define N 1000
#define TURNS 10000
int arr[N][N];

int main()
{
    double temp_time, run_time;
    std::vector<double>time_1, time_2, time_3;

    for (int iter = 0; iter < TURNS; iter++) {
        temp_time = omp_get_wtime();
        #pragma omp parallel
        {
            #pragma omp for
            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                    arr[i][j] = i * j;

        }
        run_time = omp_get_wtime() - temp_time;
        time_1.push_back(run_time);

        temp_time = omp_get_wtime();
        #pragma omp parallel
        {
            #pragma omp for nowait
            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                    arr[i][j] = i * j;

        }
        run_time = omp_get_wtime() - temp_time;
        time_2.push_back(run_time);

        temp_time = omp_get_wtime();
        #pragma omp parallel
        {
        #pragma omp for collapse(2)
            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                    arr[i][j] = i * j;

        }
        run_time = omp_get_wtime() - temp_time;
        time_3.push_back(run_time);
    }

    double sum_1{ 0.0 }, sum_2{ 0.0 }, sum_3{ 0.0 };
    for (auto iter : time_1)
        sum_1 += iter;
    for (auto iter : time_2)
        sum_2 += iter;
    for (auto iter : time_3)
        sum_3 += iter;

    std::cout << std::format("aver_1 is {:.15f}s, speedup {:.15f}%", 
        sum_1 / TURNS, 
        (sum_1 - sum_1) * 100.0 / sum_1
    ) << std::endl;
    std::cout << std::format("aver_2 is {:.15f}s, speedup {:.15f}%",
        sum_2 / TURNS,
        (sum_1 - sum_2) * 100.0 / sum_1
    ) << std::endl;
    std::cout << std::format("aver_3 is {:.15f}s, speedup {:.15f}%",
        sum_3 / TURNS,
        (sum_1 - sum_3) * 100.0 / sum_1
    ) << std::endl;

    return 0;
}

带有并行循环共享工作的Pi程序

结合前面所学内容,我们现在开始考虑带有并行循环共享工作的Pi程序:

#include <iostream>
#include <fstream>
#include <omp.h>
import <format>;

#define TURNS 100
#define PI 3.141592653589793
long double num_steps = 1e8;
double step;

int main()
{
    std::ofstream out;
    out.open("example.csv", std::ios::ate);
    out << "NTHREADS,pi,err,run_time,num_steps" << std::endl;

    double sum = 0.0;
    for (int NTHREADS = 1; NTHREADS < TURNS; NTHREADS++) {
        double start_time, run_time;
        double pi, err;

        pi = sum = 0.0;
        int actual_nthreads;

        step = 1.0 / (double)num_steps;
        omp_set_num_threads(NTHREADS);
        start_time = omp_get_wtime();

        actual_nthreads = omp_get_num_threads();
        #pragma omp parallel
        {
            double x;
            #pragma ompr for reduction(+:sum)
            for (int i = 0; i < num_steps; i++) {
                x = (i + 0.5) * step;
                sum += 4.0 / (1.0 + x * x);
            }
        }//end of parallel

        pi = step * sum;
        err = pi - PI;
        run_time = omp_get_wtime() - start_time;

        std::cout << std::format("pi is {} in {} seconds {} thrds.step is {},err is {}",
            pi,
            run_time,
            actual_nthreads,
            step,
            err
        ) << std::endl;
        out << std::format("{},{:.15f},{:.15f},{:.15f},{}",
            NTHREADS,
            pi,
            err,
            run_time,
            num_steps
        ) << std::endl;
    }
    out.close();

    return 0;
}

不过经过一系列实验过后,我们发现如此设计的并行程序效率并不是非常理想.

一种循环级并行策略

  • 首先,找到的计算密集型的循环,如此并行以抵消OpenMP调度的开销.
  • 接着,检查这些循环是否能够并行执行,通过改造循环使其消除 循环携带依赖性.

下面给出了一个通过中性转换使得循环迭代独立的例子:

int i,j,A[MAX];
j=5;
for(i =0;i<MAX;i++){
    j+=2;
    A[i]=big(j);
}
//存在循环携带依赖性的情况

int i,A[MAX];
#pragma omp parallel for
for(i=0;i<MAX;i++){
    int j = 5+2*(i+1);//消除携带依赖性的关键
    A[i]=big(j);
}
//消除了循环携带依赖性
  • 最后,应使用不同数量的线程和循环调度来优化程序.

为了分析程序并行情况,下面给出了一些OpenMP概要分析工具相关的链接:

官网: https://www.openmp.org/resources/openmp-compilers-tools/#tools

posted @ 2024-01-18 04:05  Mesonoxian  阅读(36)  评论(0编辑  收藏  举报