# 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 <iostream>
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 <iostream>
#include <omp.h> // 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;
}
運作之後, 結果是:
[email protected]:/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 <omp.h>
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);
}
運作結果如下:
[email protected]:/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<len; 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 個線程:
[email protected]:/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 于深圳