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: .
- 迭代之间相互独立、没有数据依赖
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 调度,但分块开始大,随着迭代分块越来越少,循环区间的划分是基于类似下列公式完成的(不同的编译系统可能不同):
其中 N 是线程个数, 表示第 k 块的大小, 是剩余未被调度的循环迭代次数。
- 类似于 DYNAMIC 调度,但分块开始大,随着迭代分块越来越少,循环区间的划分是基于类似下列公式完成的(不同的编译系统可能不同):
//默认调度
# 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 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了