OpenMP

OpenMP 基础 API 、归约、 parallel for 、数据依赖 重排转换、 循环调度

预备知识#

OpenMP是针对共享内存并行编程的API,系统中每个线程/进程都可能访问所有可访问的内存区域,


no
是Pthread的常见替代,更简单,但限制也更多
通过少量编译指示指出并行部分和数据共享,即可实现很多串行程序的并行化
增量并行化:程序不同部分逐步并行化
依赖编译器生成线程创建和管理代码


Hello#

#include <stdio.h>
#include <stdlib.h>
#include <omp.h> //

void Hello() {
    int my_rank = omp_get_thread_num();//线程id,0开始
    int thread_count = omp_get_num_threads();//线程数
    printf("Hello from thread %d of %d\n",my_rank,thread_count);
}
int main() {
    int thread_count=4;
# pragma omp parallel num_threads(thread_count)
    Hello();
    return 0;
}
Hello from thread 2 of 4
Hello from thread 0 of 4
Hello from thread 1 of 4
Hello from thread 3 of 4

防止编译器不支持OpenMP

#ifdef _OPENMP
	int my_rank omp_get thread_num()l
	int thread_count omp_get_num_threads();
#else
	int my_rank 0;
	int thread_count 1;
#endif

编译需要加 -fopenmp 选项

头文件 #include <omp.h>

预处理器指令以#pragma开头,提供标准C语言规范外的功能。
编译器会自动忽略不支持的编译指令。
默认只有一行,如果一行放不下,新的一行前面加转义字符\

# pragma omp parallel之后是一个代码块,块中的代码并行执行,系统自动取派生线程,执行完后合并,这里有一个隐式路障,会等待线程组中所有线程都完成,主线程才会继续往下执行。

变量的作用域#

每个线程执行相同的代码(SPMD),但都有自己的栈,
shared 变量是共享的
private 变量是私有的
默认是 shared,循环变量是 private

梯形积分法#

临界区指令:每个时刻限制只有一个线程执行
#pragma omp critical [ ( name )]

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
double f(double x) {
    return x * x + x;
}
double Trap(double a, double b, int n) {
    double h, x, my_result;
    double local_a, local_b;
    int i, local_n;
    int my_rank = omp_get_thread_num();
    int thread_count = omp_get_num_threads();

    h = (b - a) / n;
    local_n = n / thread_count;
    local_a = a + my_rank * local_n * h;
    local_b = local_a + local_n * h;

    my_result = (f(local_a) + f(local_b)) / 2.0;
    for (i = 1; i < local_n; i++) {
        x = local_a + i * h;
        my_result += f(x);//为避免过多通信,先求局部和
    }

    return my_result * h;
}

int main(int argc, char *argv[]) {
    double global_result = 0.0;
    double a, b;
    int n;
    int thread_count;
    thread_count = 8;
    printf("Enter a,b,n\n");
    scanf("%lf %lf %d", &a, &b, &n);

# pragma omp parallel num_threads(thread_count) {
	double my_result=0.0;
	my_result += Trap(a, b, n);//不同线程并行计算
	# pragma omp critical
		global_result += my_result;//只有求和全局串行。
}

    printf("%lf", global_result);
    return 0;
}

归约子句#

规约(reduction) 是一种常用的并行操作,用于解决多个线程对某个共享变量进行累积操作(如求和、求积、最大值、最小值等)时的并发问题。规约操作将多个线程的局部结果归并到一个全局变量中,同时确保线程间不会出现竞争条件。
基本语法:
#pragma omp parallel reduction(operator : variable)

  • operator:表示规约操作符,OpenMP 支持多种规约操作符,例如 +(加法)、*(乘法)、max(最大值)、min(最小值)、&|^等。
  • variable:需要进行规约操作的变量,必须是一个标量。
    第37——42行可以简化为以下代码:
#pragma omp parallel num_threads(thread_count) reduction(+ : global_result)
    global_result += Trap(a, b, n);

parallel for 并行循环#

Q: parallel for使用场景:
A: .

  1. 迭代之间相互独立、没有数据依赖
for (int i = 0; i < N-1; i++) { 
	a[i+1] = a[i] + 1; // 迭代之间有依赖关系,就不能用
}

(否则可能会产生竞争条件,导致错误或不可预知的结果。)
2. 要求迭代次数可预测。
(可能需要动态调整任务分配,会导致负载不均、线程过多或过少,从而影响程序的性能。)
3. 不支持 while 、 do while 、循环体包含 break 等
(条件的判断是动态的,无法预先确定。此外,break 语句的存在可能导致循环提前退出,这样在某些情况下并行执行的线程将无法正确地同步或完成其任务。)

梯形积分法19-23行代码可改为:

    my_result = (f(local_a) + f(local_b)) / 2.0;
    #pragma omp parallel for num_threads(thread_count) reduction(+:my_result)
    for (i = 1; i < local_n; i++) {
        x = local_a + i * h;
        my_result += f(x);
    }

同步#

  • 隐式barrier
    • 并行结构开始和结束的地方
    • 其他控制结构结束位置
  • 显式同步
    • critical
    • atomic

重排转化:该百年语句执行顺序吗,不增删语句,保持依赖关系
体会下面的:

for(i=2;i<5;i++)
	a[i]=a[i-2]+1;

有数据依赖

for(i=2;i<5;i++) {
	a[i]=i+1;
	b[i]=a[i]*3;
}

无数据依赖

估算π#

用openMP实现上一章的估算π算法

factor是私有的

double sum = 0.0;

#pragma omp parallel for num_threads(thread_count) reduction(+:sum) private(factor)
for (k = 0; k < n; k++) {
    factor = (k % 2 == 0) ? 1.0 : -1.0;
    sum += factor / (2 * k + 1);
}

pi_approx = 4.0 * sum;

更多OpenMP的循环:排序#

for (list_length= n; list_length >= 2; list_length)
    for (i = 0; i < list_length; i++)
        if (a[i] > a[i+1]) {
            tmp = a[i];
            a[i] = a[i+1];
            a[i+1] = tmp;
        }

显然其存在依赖,求前一轮的比较结果必须先确定了,后一轮才能开始

引入奇偶转置排序
交替比较相邻元素对来逐步排序数组,比如一共6个数,对于奇数轮,只比较([1,2],[3,4],[5,6]);对于偶数轮,比较([2,3],[4,5]),这样显然是可以并发的

  • 完成一次奇数步骤和一次偶数步骤后,数组中的最大元素会被放置在数组的最后一个位置。
  • 然后算法重复奇数步骤和偶数步骤,但是不再考虑已经排序好的最大元素。
  • 这个过程会一直重复,直到整个数组排序完成。
void odd_even_sort(int a[], int n, int thread_count) {
    int phase, i, tmp;
    #pragma omp parallel num_threads(thread_count) \
        default(none) shared(a, n) private(i, tmp, phase)
    for (phase = 0; phase < n; phase++) {
        if (phase % 2 == 0) { 
            #pragma omp for
            for (i = 1; i < n; i += 2) {  
                if (a[i] > a[i + 1]) {
                    tmp = a[i];
                    a[i] = a[i + 1];
                    a[i + 1] = tmp;
                }
            }
        } 
        else {
            #pragma omp for
            for (i = 0; i < n - 1; i += 2) {  
                if (a[i] > a[i + 1]) {
                    tmp = a[i];
                    a[i] = a[i + 1];
                    a[i + 1] = tmp;
                }
            }
        }
    }
}

循环调度#

schedule 子句确定如何在线程间划分循环

  • static ([chunk]) 静态划分

    • 分配给每个线程 [chunk] 步迭代,所有线程都分配完后继续循环分配,直至所有迭代步分配完毕
    • 默认 [chunk]ceil(#iterations/#threads)
  • dynamic ([chunk]) 动态划分

    • 分给每个线程 [chunk] 步迭代,一个线程完成任务后再为其分配 [chunk] 步迭代
    • 逻辑上形成一个任务池,包含所有迭代步
    • 默认 [chunk] 为 1
  • guided ([chunk]) 动态划分,但划分过程中 [chunk] 指数减小

    • 类似于 DYNAMIC 调度,但分块开始大,随着迭代分块越来越少,循环区间的划分是基于类似下列公式完成的(不同的编译系统可能不同):
      Sk=Rk2N
      其中 N 是线程个数,Sk 表示第 k 块的大小,Rk 是剩余未被调度的循环迭代次数。
//默认调度
# pragma omp parallel for num_threads(thread_count) reduction(+:sum)
	for (i = 0; i <= n; i++)
		sum += f(i);
//等价于
# pragma omp parallel for num_threads(thread_count) reduction(+:sum) \
\schedule(static,n/thread_count)
	for (i = 0; i <= n; i++)
		sum += f(i);

作者:AuroraKelsey

出处:https://www.cnblogs.com/AuroraKelsey/p/18669402

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   AuroraKelsey  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示