天天看點

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 <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 于深圳

繼續閱讀