前兩天(實際上是幾個月之前了)看到了代碼中有 #pragma omp parallel for
一段,感受好像是 OpenMP,之前看到並行化的東西都是直接躲開,既然躲不開了,不妨研究一下:ios
OpenMP 是 Open MultiProcessing 的縮寫。OpenMP 並非一個簡單的函數庫,而是一個諸多編譯器支持的框架,或者說是協議吧,總之,不須要任何配置,你就能夠在 Visual Studio 或者 gcc 中使用它了。程序員
咱們就分三部分來介紹吧,由於我看的那個英文教程就是分了三部分(哈哈) . 如下翻譯自英特爾的文檔算法
把下面的代碼保存爲 omp.cc數組
#include <iostream> #include <omp.h> int main() { #pragma omp parallel for for (char i = 'a'; i <= 'z'; i++) std::cout << i << std::endl; return 0; }
而後 g++ omp.cc -fopenmp
就能夠了數據結構
OpenMP的設計們但願提供一種簡單的方式讓程序員不須要懂得建立和銷燬線程就能寫出多線程化程序。爲此他們設計了一些pragma,指令和函數來讓編譯器可以在合適的地方插入線程大多數的循環只須要在for以前插入一個pragma就能夠實現並行化。並且,經過把這些惱人的細節都丟給編譯器,你能夠花費更多的時間來決定哪裏須要多線程和優化數據結構多線程
下面個這個例子把32位的RGB顏色轉換成8位的灰度數據,你只須要在for以前加上一句pragma就能夠實現並行化了負載均衡
#pragma omp parallel for for (int i = 0; i < pixelCount; i++) { grayBitmap[i] = (uint8_t)(rgbBitmap[i].r * 0.229 + rgbBitmap[i].g * 0.587 + rgbBitmap[i].b * 0.114); }
神奇吧,首先,這個例子使用了「work sharing」,當「work sharing」被用在for循環的時候,每一個循環都被分配到了不一樣的線程,而且保證只執行一次。OpenMP決定了多少線程須要被打開,銷燬和建立,你須要作的就是告訴OpenMP哪裏須要被線程化。框架
OpenMP 對能夠多線程化的循環有以下五個要求:函數
若是你的循環不符合這些條件,那就只好改寫了oop
檢測是否支持 OpenMP
#ifndef _OPENMP fprintf(stderr, "OpenMP not supported"); #endif
當一個循環知足以上五個條件時,依然可能由於數據依賴而不可以合理的並行化。當兩個不一樣的迭代之間的數據存在依賴關係時,就會發生這種狀況。
// 假設數組已經初始化爲1 #pragma omp parallel for for (int i = 2; i < 10; i++) { factorial[i] = i * factorial[i-1]; }
編譯器會把這個循環多線程化,可是並不能實現咱們想要的加速效果,得出的數組含有錯誤的結構。由於每次迭代都依賴於另外一個不一樣的迭代,這被稱之爲競態條件。要解決這個問題只可以重寫循環或者選擇不一樣的算法。
競態條件很難被檢測到,由於也有可能剛好程序是按你想要的順序執行的。
基本上每一個循環都會讀寫數據,肯定那個數據時線程之間共有的,那些數據時線程私有的就是程序員的責任了。當數據被設置爲公有的時候,全部的線程訪問的都是相同的內存地址,當數據被設爲私有的時候,每一個線程都有本身的一份拷貝。默認狀況下,除了循環變量之外,全部數據都被設定爲公有的。能夠經過如下兩種方法把變量設置爲私有的:
// 下面這個例子是錯誤的 int temp; // 在循環以外聲明 #pragma omp parallel for for (int i = 0; i < 100; i++) { temp = array[i]; array[i] = doSomething(temp); }
能夠經過如下兩種方法改正
// 1. 在循環內部聲明變量 #pragma omp parallel for for (int i = 0; i < 100; i++) { int temp = array[i]; array[i] = doSomething(temp); }
// 2. 經過OpenMP指令說明私有變量 int temp; #pragma omp parallel for private(temp) for (int i = 0; i < 100; i++) { temp = array[i]; array[i] = doSomething(temp); }
一種常見的循環就是累加變量,對此,OpenMP 有專門的語句
例以下面的程序:
int sum = 0; for (int i = 0; i < 100; i++) { sum += array[i]; // sum須要私有才能實現並行化,可是又必須是公有的才能產生正確結果 }
上面的這個程序裏,sum公有或者私有都不對,爲了解決這個問題,OpenMP 提供了reduction語句;
int sum = 0; #pragma omp parallel for reduction(+:sum) for (int i = 0; i < 100; i++) { sum += array[i]; }
內部實現中,OpenMP 爲每一個線程提供了私有的sum變量,當線程退出時,OpenMP 再把每一個線程的部分和加在一塊兒獲得最終結果。
固然,OpenMP 不止能作累加,凡是累計運算都是能夠的,以下表:
操做 | 私有臨時變量初值 |
---|---|
+、- | 0 |
* | 1 |
& | ~0 |
| |
0 |
^ | 0 |
&& | 1(true) |
|| |
0(false |
負載均衡是多線程程序中對性能影響最大的因素了,只有實現了負載均衡才能保證全部的核心都是忙的,而不會出現空閒時間。若是沒有負載均衡, 有一些線程會遠遠早於其餘線程結束, 致使處理器空閒浪費優化的可能.
在循環中,常常會因爲每次迭代的相差時間較大和破壞負載平衡。一般能夠經過檢查源碼來發現循環的變更可能. 大多數狀況下每次迭代可能會發現大概一致的時間,當這個條件不能知足的時候,你可能能找到一個花費了大概一致時間的子集。例如, 有時候全部偶數循環花費了和全部奇數循環同樣的時間, 有時候可能前一半循環和後一半循環花費了類似的時間. 另外一方面, 有時候你可能找不到花費相同時間的一組循環. 不論如何, 你應該把這些信息提供給 OpenMP, 這樣才能讓 OpenMP 有更好的機會去優化循環.
默認狀況下,OpenMP認爲全部的循環迭代運行的時間都是同樣的,這就致使了OpenMP會把不一樣的迭代等分到不一樣的核心上,而且讓他們分佈的儘量減少內存訪問衝突,這樣作是由於循環通常會線性地訪問內存, 因此把循環按照前一半後一半的方法分配能夠最大程度的減小衝突. 然而對內存訪問來講這多是最好的方法, 可是對於負載均衡可能並非最好的方法, 並且反過來最好的負載均衡可能也會破壞內存訪問. 所以必須折衷考慮.
OpenMP 負載均衡使用下面的語法
#pragma omp parallel for schedule(kind [, chunk size])
其中kind能夠是下面的這些類型, 而 chunk size 則必須是循環不變的正整數
#pragma omp parallel for for (int i = 0; i < numElements; i++) { array[i] = initValue; initValue++; }
顯然這個循環裏就有了競態條件, 每一個循環都依賴於 initValue 這個變量, 咱們須要去掉它.
#pragma omp parallel for for (int i = 0; i < numElements; i++) { array[i] = initValue + i; }
這樣就能夠了, 由於如今咱們沒有讓 initValue 去被依賴
因此, 對於一個循環來講, 應該儘量地把 loop-variant 變量創建在 i 上.
待續...