付哥和你说旅游
当前位置:首页 - 专栏 >

正文 | OpenMP 并行计算入门案例 | 火热

2019-11-30来源:国际财经网
正文 | OpenMP 并行计算入门案例 | 火热

原创: jidor 泛智能时代


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 _OPENMP
cout << " support openmp " << endl;
#else
cout << " not support openmp" << endl;
#endif
return 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 ADD
using namespace std;
int main()
{
#pragma omp parallel for num_threads(4) // NEW ADD
for(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
{
auto &e = v_i[i];
int t = 0;
if ( i < len / 2)
{
t = pow(e, 2);
}
else
{
t = (int)sqrt(e);
}
if ( t % 2 == 1)
{
cnt_ans += 1;
}
}

我在的机器上运行,开 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的精确分配与控制, 都非常灵活简洁,这里只带同学们入门,更多精彩,还需要自己去运行改动代码调试,去体验。

无数风光在险峰!

如果关于文章其它的问题,也可以关注我的weix公号 泛智能时代 ( id: jd_geek) 与我交流。


泛智能时代

谢谢关注。


林奇思妙想

2019.03.24 于深圳



转载文章地址:http://www.cpteststations.com/zhuanlan/30056.html
(本文来自付哥和你说旅游整合文章:http://www.cpteststations.com)未经允许,不得转载!
标签:
并行计算 GCC Windows 设计 C语言 Linux Git Microsoft Visual Studio Ubuntu GitHub 集成开发环境
网站简介 联系我们 网站申明 网站地图

版权所有:www.cpteststations.com ©2017 付哥和你说旅游

付哥和你说旅游提供的所有内容均是网络转载或网友提供,本站仅提供内容展示服务,不承认任何法律责任。