OpenMP 并行计算入门案例
# OpenMP 并行计算入门案例
文章目录
- OpenMP 设计哲学和优点
- 环境要求
- Windows / Visual Studio 平台
- Linux / GCC 平台
- 示例源代码
- 验证支持 OpenMP
- 可并行前提
- 实例练习
- 入门示例:并行输出
- 并行输出, 非 OpenMP 实现
- OpenMP 求累加和
- 获取线程索引 id
- 原子操作与同步
- 高阶实例演示
- 后 记
OpenMP 设计哲学和优点
OpenMP 是一套 C++ 并行编程框架, 也支持 Forthan .
它是一个跨平台的多线程实现, 能够使串行代码经过最小的改动自动转化成并行的。具有广泛的适应性。这个最小的改动,有时候只是一行编译原语!(在高阶示例中,我们将演示并评估加速性能)
具体实现是通过分析编译原语#pragma,将用原语定义的代码块,自动转化成并行的线程去执行。每个线程都将分配一个独立的id. 最后再合并线程结果。
问题来了,学习 OpenMP , 我们怎么开始?
环境要求
在开始之前,我们先确定一下我们的 C++ 编译环境能不能支持 OpenMP.
Windows / Visual Studio 平台
VS 版本不低于2015,都支持 OpenMP .
需要在 IDE 进行设置,才能打开 OpenMP 支持。
设置方式:
调试->C/C+±>语言->OpenMP支持
这实际上使用了编译选项/openmp。
Linux / GCC 平台
本人用的 Ubuntu 16.04 (经典版) 自带的GCC 5.0.4, 直接支持选项-fopenmp.
示例源代码
本节所有的代码均放在个人 Github:
tlqtangok > openmp_demo
git clone https://github.com/tlqtangok/openmp_demo.git
验证支持 OpenMP
如果已经设置好了,我们就开始编程来 Double Check 一下吧。
参考目录 00_dep
#include
using namespace std;int main()
{#if _OPENMPcout << " support openmp " << endl;
#elsecout << " not support openmp" << endl;
#endifreturn 0;
}
运行代码:
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o mainapp.exe main.o
./mainapp.exe
运行输出
support openmp
即表明我们支持 OpenMP 了。
接下来我们就可以进入正题了。
可并行前提
要想并行,就需要满足如下的条件:
-
可拆分
代码和变量的前后不能相互依赖。
-
独立运行
运行时,拥有一定的独有的资源。像独有的线程id等。
其它理论的知识,请自行阅读计算机体系结构之类的计算机基础。 本 chat 着重讲讲实际操作技能。
实例练习
入门示例:并行输出
目录 01_parallel_cout
- 正常顺序输出
0 ~ 10
for(int i=0; i< 10; i++)
{cout << i << endl;
}
这样子,0~10 是顺序打印的。
我们要并行的运行打印,即乱序的输出 0~10 才能证明并行运行(而且运行结果不一定一致)。
- 用 OpenMP 容易实现并行输出
#include
#include // NEW ADDusing namespace std;int main()
{#pragma omp parallel for num_threads(4) // NEW ADDfor(int i=0; i<10; i++){cout << i << endl;}return 0;
}
运行之后, 结果是:
jd@ubuntu:/mnt/hgfs/et/git/openmp_demo/01_parallel_cout$ make run
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o mainapp.exe main.o
./mainapp.exe
3
4
5
8
9
6
7
0
1
2
可以看到乱序了!说明我们的 OpenMP 并行起了作用。
这里只多加了两行代码,就并行化了这个任务。是不是很简洁。
解析上面新加的两行:
#include
OpenMP 的编译头文件,包括一些常用API,像获取当前的线程id.
#pragma omp parallel for num_threads(4)
用编译原语,指定其下面的代码块将会被渲染成多线程的代码,然后再编译。这里使用的线程数为 4。对比一下不用 OpenMP 的代码,你一定会感叹, OpenMP 真香。
并行输出, 非 OpenMP 实现
为了模拟自己实现 OpenMP 的 for 框架, 我做了以下尝试…
总之, 代码量不少,约 135 行,还很容易出错。作为对比,大家运行一下,看看就好。
具体请看目录 02_parallel_no_omp.
OpenMP 求累加和
代码请看 03_reduce
求1~100 之和, 用 32 个线程并行
int sum = 0;#pragma omp parallel for num_threads(32)for(int i=0; i<100; i++){sum += i; }cout << sum << endl;
标准答案是 4950, 但是,运行的结果有时候是 4950, 有时候却不是。
为什么呢?
因为其中产生了竞争。
sum += i; 这一行如果多个线程同时写,可能会发生写冲突。
关于这些 reduce 的问题,OpenMP 也有专门的原语来帮我们,让我们小小地改动一下就行了。
int sum = 0;#pragma omp parallel for num_threads(32) reduction(+:sum)for(int i=0; i<100; i++){sum += i; }cout << sum << endl;
我们只要亮出sum 是要保护的 reduce 变量就可以了 !
获取线程索引 id
每个线程都有自己的身份,表明他是第几号。
获取这个第几号可以方便调试。
在 OpenMP 中,这个id很容易获取。本人一般喜欢用idx来表示这个id.
请看 04_get_idx
#define DEFINE_idx auto idx = omp_get_thread_num();
#define _ROWS (omp_get_num_threads())
在块内定义idx, 就可以用了:
#pragma omp parallel for num_threads(3) for(int i=0; i<10; i++){DEFINE_idx;printf("- idx is %d, i is %d, total thread num is %d\n", idx, i, _ROWS); }
运行结果如下:
jd@ubuntu:/mnt/hgfs/et/git/openmp_demo/04_get_idx$ make run
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o mainapp.exe main.o
./mainapp.exe
- idx is 0, i is 0, total thread num is 3
- idx is 0, i is 1, total thread num is 3
- idx is 0, i is 2, total thread num is 3
- idx is 0, i is 3, total thread num is 3
- idx is 2, i is 7, total thread num is 3
- idx is 2, i is 8, total thread num is 3
- idx is 2, i is 9, total thread num is 3
- idx is 1, i is 4, total thread num is 3
- idx is 1, i is 5, total thread num is 3
- idx is 1, i is 6, total thread num is 3
idx < _ROWS, _ROWS 是使用的线程数。
原子操作与同步
参考目录:05_atomic_barrier
int sum = 0;
#pragma omp parallel num_threads(3) {
#pragma omp atomic sum += 10; #pragma omp barrier // TODO : disable this to see cout << sum << endl; }
其中的原子atomic,与之前讲的 reduce 变量有相同的内涵, 都是为了防止并发写引起的竞争。
#pragma omp barrier 是为了使所有的线程都在这一处 join, 这一点像施工的甘特图, 公司 A 做完项目 P0 后,必须等其它公司全部完成,然后大家再一起开工项目 P1. (可以试试去掉 barrier 看看会发生什么)
高阶实例演示
最后我们再来一个综合的例子,尽量把所学习的 OpenMP 知识,都融合进去。
任务:
对于一个大向量(所有元素全部大于 0), 把它的前半部分全部平方,后半部分全部开方取整。将所得的新向量中的奇数个数输出
项目请看:
06_mix
#pragma omp parallel for reduction(+:cnt_ans) default(shared) num_threads(10)for(int i=0; i
我在的机器上运行,开 10 个线程:
jd@ubuntu:/mnt/hgfs/et/git/openmp_demo/06_mix$ make run
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11 -g -pthread -Wno-format -fpermissive -fopenmp -o mainapp.exe main.o
./mainapp.exe
- time used: 0.029141 seconds
- size of v_ans: 2522908
注释行 #pragma omp parallel for reduction ... num_threads(12) ,运行时间为:
./mainapp.exe
- time used: 0.080039 seconds
- size of v_ans: 2522908
约为前者 3~4 倍, 由此可见, OpenMP 很有优势的,一条原语,能加速到相当的程度。
后 记
OpenMP 还有很多高级原语, 限于入门课程,还是不要把人整懵为好: ) .
更高级的特性,大多与线程和临界资源的精细控制相关, 包括锁,任务同步,临界点控制,idx的精确分配与控制, 都非常灵活简洁,这里只带同学们入门,更多精彩,还需要自己去运行改动代码调试,去体验。
无数风光在险峰!
谢谢关注。
林奇思妙想
2019.03.24 于深圳
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
