OpenMP支持c、cpp、fortran,本文對比使用openmp和未使用openmp的效率差距和外在表現,而後講解基礎知識。ios
1、舉例
一、使用OpenMP與未使用OpenMP的比較。c++
OpenMP是使用多線程的接口。windows
以c語言程序舉例,即ba.c文件以下:api
#include <omp.h> #include <stdio.h> #include <stdlib.h> #include <windows.h> void Test(int n) { int j; for (int i = 0; i < 100000000; ++i) { //do nothing, just waste time j++; } printf("%d, ", n); } int main(int argc, char *argv[]) { int i; #pragma omp parallel for for (i = 0; i < 100; ++i) Test(i); system("pause"); return 1; }
在編譯時,參數以下:多線程
編譯結果以下:框架
耗時:9side
注意:個人電腦爲雙核,因此開啓了4個線程分別運行。函數
接下來,我經過window + R,輸入msconfig,並進入boot中的高級設置,將我電腦的設置爲單核,而後再運行一樣的程序,能夠發現結果以下:性能
因而能夠發現,再設置爲單核以後,程序會建立兩個線程,這樣的結果就是從0 50開始劃分,這樣顯然是沒有充分利用cpu的,因此將電腦設置爲原來的雙核。優化
未優化的以下:
// #include <omp.h> #include <stdio.h> #include <stdlib.h> #include <windows.h> void Test(int n) { int j; for (int i = 0; i < 100000000; ++i) { //do nothing, just waste time j++; } printf("%d, ", n); } int main(int argc, char *argv[]) { int i; // #pragma omp parallel for for (i = 0; i < 100; ++i) Test(i); system("pause"); return 1; }
在編譯時,參數以下:
編譯結果以下:
耗時: 24s
不可貴知,此程序使用的爲單核單線程 ,因此運行速度遠遠低於使用多核多線程的速度。
上面輸出了100個數字,時間上來講優化後是未優化的3倍。
而後後續我又輸出了1000個數字,未優化的時間爲240s,而優化後的時間爲84s,可見優化後一樣也是優化前的3倍左右。
之因此四個線程的速度僅僅是單線程速度的3倍而不是4倍,這是由於多線程在線程的切換、合做等方面也須要花費必定的時間,因此只是到了3倍的差距,而沒有達到4倍的差距。
二、獲取當前線程id、獲取總的線程數
#include <omp.h> #include <stdio.h> #include <stdlib.h> #include <windows.h> int main(int argc, char *argv[]) { int nthreads,tid; // fork a team of thread #pragma omp parallel private(nthreads,tid) { //obtian and print thread id tid=omp_get_thread_num(); printf("Hello Word from OMP thread %d\n",tid); // only master thread does this; if(tid==0) { nthreads = omp_get_num_threads(); printf("Number of thread: %d\n",nthreads); } } system("pause"); return 1; }
編譯條件以下:
運行結果以下:
每次運行,能夠發現順序是不一樣的。可是Number of thread: 4永遠是在線程0以後出現,而且tid==0時的這個線程爲主線程。
三、以前使用的爲c語言,下面改寫爲c++。
#include <omp.h> #include <iostream> #include <windows.h> using namespace std; void Test(int n) { int j; for (int i = 0; i < 100000000; ++i) { //do nothing, just waste time j++; } cout << n << " "; } int main(int argc, char *argv[]) { int i; #pragma omp parallel for for (i = 0; i < 100; ++i) Test(i); system("pause"); return 1; }
編譯條件爲 g++ t.cpp -o t -fopenmp,結果以下:
一樣地,這裏使用四核來生成的。
四、以下所示,也是使用c++語言。
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; void test(int m) { int i = 0; double a = 0.0; double b = 0.0; double c = 0.0; for (i = 0; i < 100000000; i++) { a += 0.1; b += 0.2; c = a + b; } cout << m << " "; } int main() { #pragma omp parallel for for (int i = 0; i < 200; i++) { test(i); } system("pause"); return 0; }
在這裏也沒有什麼很大的區別,總之,咱們就是須要將void test這個函數寫的複雜一些,而後就會耗時,這樣才能看出來變化。
另外,在這裏,咱們能夠看到結果中,是對for循環進行等量的劃分,好比對於i從0到200的for循環裏,會根據我電腦的2核劃分爲0 - 50、 51-100、 101-150、 151-200這幾個區間,而後使用多核cpu進行運算,這個優化的效果我想是很是驚人的。
五、三層循環
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; void test(int m, int n, int l) { int i = 0; double a = 0.0; double b = 0.0; double c = 0.0; for (i = 0; i < 100000000; i++) { a += 0.1; b += 0.2; c = a + b; } cout << m << " " << n << " " << l << endl; } int main() { cout << "first" << endl; #pragma omp parallel for for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { for (int k = 0; k < 5; k++) { test(i, j, k); } } } cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "over hah" << endl; cout << "second" << endl; #pragma omp parallel for for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { for (int k = 0; k < 5; k++) { test(i, j, k); } } } system("pause"); return 0; }
結果,
能夠看到,仍是最外層的循環進行轉化。
耗時: 45s。
若是咱們將上面的兩個#pragma openmp parallel for去掉,再進行試驗。注意#pragma是預編譯指令,好比這裏告訴編譯器要進行並行運算。
則須要100s左右。雖然沒有特別明顯的提升,可是仍是快了不少,優點是很是明顯的。
上面的舉例都是一些簡單的例子,而對於具體的項目還會遇到問題,須要靈活應變。
2、基礎
須要使用openmp就須要引入omp.h庫文件。而後在編譯時添加參數 -fopenmp便可。 在具體須要進行並行運算的部分,使用 #pragma omp 指令[子句] 來告訴編譯器如何並行執行對應的語句。 經常使用的指令以下:
- parallel - 即#pragma omp parallel 後面須要有一個代碼片斷,使用{}括起來,表示會被並行執行。
- parallel for - 這裏後面跟for語句便可,不須要有額外的代碼塊。
- sections
- parallel sections
- single - 表示只能單線程執行
- critical - 臨界區,表示每次只能有一個openmp線程進入
- barrier - 用於並行域內代碼的線程同步,線程執行到barrier時停下來 ,直到全部線程都執行到barrier時才繼續。
經常使用的子句以下:
- num_threads - 指定並行域內線程的數目
- shared - 指定一個或者多個變量爲多個線程的共享變量
- private - 指定一個變量或者多個變量在每一個線程中都有它的副本
另外,openmp還提供了一些列的api函數來獲取並行線程的狀態或控制並行線程的行爲,經常使用api以下:
- omp_in_parallel - 判斷當前是否在並行域中。
- omp_get_thread_num - 獲取線程號
- omp_set_num_threads - 設置並行域中線程格式
- omp_get_num_threads - 返回並行域中線程數
- omp_get_dynamic - 判斷是否支持動態改變線程數目
- omp_get_max_threads - 獲取並行域中可用的最大的並行線程數目
- omp_get_num_procs - 返回系統中處理器的個數
一、以下使用parallel,會根據電腦配置並行執行屢次。
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; int main() { #pragma omp parallel { cout << "this is in parallel" << endl; } system("pause"); return 0; }
二、使用parallel num_threads(3),限制並行的線程數爲3。
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; int main() { #pragma omp parallel num_threads(3) { cout << "this is in parallel" << endl; } system("pause"); return 0; }
這樣,最終會輸出3個語句,由於語句被並行運行了3次。結果以下:
可是上面的結果不是固定的,這裏能夠很明顯的表示出程序是並行運行的,由於第一個輸出還沒來得及換行,第二個又繼續輸出了,因此它們是獨立地並行地運算的。
三、下面咱們使用 #pragma parallel for num_threads(4),而且在並行域中,咱們還經過 omp_get_thread_num()來獲取線程號,以下:
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; int main() { #pragma omp parallel for num_threads(4) for (int i = 0; i < 20; i++) { cout << omp_get_thread_num() << endl; } system("pause"); return 0; }
這裏就是對這個for循環使用4個線程來並行。 注意 #pragma omp parallel for num_threads(4) 與 #pragma omp parallel num_threads(4) 不一樣,可自行體會。
結果以下,出現空行是由於多線程並行運算,致使換行符沒來得及輸出另一個線程號就被輸出了。
四、對比單線程、2線程、4線程、...... 、12線程效率。
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; void test() { int j = 0; for (int i = 0; i < 100000; i++) { // do something to kill time... j++; } }; int main() { double startTime; double endTime; // 不使用openMp startTime = omp_get_wtime(); for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "single thread cost time: " << endTime - startTime << endl; // 2個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(2) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "2 threads cost time: " << endTime - startTime << endl; // 4個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(4) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "4 threads cost time: " << endTime - startTime << endl; // 6個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(6) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "6 threads cost time: " << endTime - startTime << endl; // 8個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(8) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "8 threads cost time: " << endTime - startTime << endl; // 10個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(10) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "10 threads cost time: " << endTime - startTime << endl; // 12個線程 startTime = omp_get_wtime(); #pragma omp parallel for num_threads(12) for (int i = 0; i < 100000; i++) { test(); } endTime = omp_get_wtime(); cout << "12 threads cost time: " << endTime - startTime << endl; system("pause"); return 0; }
結果以下:
因而,咱們能夠看到,單線程(不使用openMP)時消耗時間最長,2線程約爲單線程的一半,4個線程(本電腦爲4個邏輯內核)約爲1/3時間,6個線程的時候時間甚至更長,12個線程在時間上也沒有明顯額減小,因此,線程數的制定能夠根據電腦的核心數來作出選擇。
更多例子:
#include <iostream> #include <windows.h> #include <omp.h> using namespace std; void test(int i) { int j = 0; for (int i = 0; i < 100000000; i++) { // do something to kill time... j++; } cout << i << endl; }; int main() { // 此程序中使用到的openmp不可簡單地理解爲一個封裝起來的庫,實際上應該理解爲一個框架。這個框架是由硬件開發商和軟件開發商共同開發的,即經過協商api,來使得多核並行運算更容易上手、使用 // 主要參考文章:https://wdxtub.com/2016/03/20/openmp-guide/ // #pragma omp parallel for // for (int i = 0; i != 10; i++) { // test(i); // } // #pragma omp parallel // { // #pragma omp for // for (int i = 0; i < 10; i++) { // test(i); // } // } // XXX 錯誤,對於並行運算,不支持 != 的形式 // #pragma omp parallel for // for (int i = 0; i != 10; i++) { // test(i); // } // 在並行區域內聲明瞭一個變量private_a,那麼在多線程執行時,每一個線程都會建立這麼一個private_a變量。 // 最終輸出結果爲668/668/669/669,說明2個線程加了兩次,2個線程加了3次。 // #pragma omp parallel // { // int private_a = 666; // #pragma omp for // for (int i = 0; i < 10; i++) { // test(i); // private_a++; // } // cout << private_a << endl; // } // 在並行區域以外定義的變量是共享的,即便下面有多個線程並行執行for循環,可是不會爲每一個線程建立share_a變量,因此最終每一個線程訪問的都是同一個內存,輸出的結果爲4個676 // int share_a = 666; // #pragma omp parallel // { // #pragma omp for // for (int i = 0; i < 10; i++) { // test(i); // share_a++; // } // cout << share_a << endl; // } // 注意:這種循環是普通的循環,其中的sum是共享的,而後sum是累加的,因此從結果中也能夠看出sum必定是非遞減的,最終結果爲45。 // int sum = 0; // cout << "Before: " << sum << endl; // #pragma omp parallel for // for (int i = 0; i < 10; i++) { // sum = sum + i; // cout << sum << endl; // } // cout << "After: " << sum << endl; // 注意:這裏採用了reduction(+:sum),因此每一個線程根據reduction(+:sum)的聲明計算出本身的sum(注意:在每一個線程計算之初,sum均爲在並行域以外規定的0,即對於4個線程而言,4個線程都會有一個初始值爲0的sum,而後再疊加),而後再將各個線程的sum添加起來,因此從結果來看,sum是不存在某種特定規律的。 // int sum = 0; // cout << "Before: " << sum << endl; // #pragma omp parallel for reduction(+:sum) // for (int i = 0; i < 10; i++) { // sum = sum + i; // cout << sum << endl; // } // cout << "After: " << sum << endl; // 下面的減法是相似的,對比上面的兩個例子便可。 // int sum = 100; // cout << "Before: " << sum << endl; // #pragma omp parallel for // for (int i = 0; i < 10; i++) { // sum = sum - i; // cout << sum << endl; // } // cout << "After: " << sum << endl; // int sum = 100; // cout << "Before: " << sum << endl; // #pragma omp parallel for reduction(-:sum) // for (int i = 0; i < 10; i++) { // sum = sum - i; // cout << sum << endl; // } // cout << "After: " << sum << endl; // 下面的兩個例子中一個使用了原子操做,一個沒有使用原子操做。 // 使用原子操做的最後結果正確且穩定,而沒有使用原子操做最終的結果是不穩定的。 // int sum = 0; // cout << "Before: " << sum << endl; // #pragma omp parallel for // for (int i = 0; i < 20000; i++) { // #pragma omp atomic // sum++; // } // cout << "Atomic-After: " << sum << endl; // int sum = 0; // #pragma omp parallel for // for (int i = 0; i < 20000; i++) { // sum++; // } // cout << "None-atomic-After: " << sum << endl; // 線程同步之critical // 使用critical獲得的結果是穩定的,而不使用critical獲得的結果是不穩定的。 // 值得注意的是:critical與atomic的區別在於 - atomic僅僅使用自增(++、--等)或者簡化(+=、-=等)兩種方式, // 而且只能表示下一句,而critical卻沒有限制,且能夠經過{}代碼塊來表示多句同時只能有一個線程來訪問。 // int sum = 0; // cout << "Before: " << sum << endl; // #pragma omp parallel for // for (int i = 0; i < 100; i++) { // #pragma omp critical(a) // { // sum = sum + i; // sum = sum + i * 2; // } // } // cout << "After: " << sum << endl; // 同時運行下面的兩個程序,能夠發現有些許不一樣。 // 這個程序中的第一個for循環會多線程執行,而且若是一個線程執行完,若是有的線程沒有執行完, // 那麼就會等到全部線程執行完了再繼續向下執行。因此結果中 - 和 + 區分清晰。 // #pragma omp parallel // { // #pragma omp for // for (int j = 666; j < 1000; j++) { // cout << "-" << endl; // } // #pragma omp for nowait // for (int i = 0; i < 100; i++) { // cout << "+" << endl; // } // } // 這個程序中的第一個for循環一樣會有多個線程同時執行,只是其中某個線程最早執行完了以後, // 不會等其餘的線程,而是直接進入了下一個for循環,因此結果中的 - 和 + 在中間部分是混雜的。 // #pragma omp parallel // { // #pragma omp for nowait // for (int i = 0; i < 100; i++) { // cout << "+" << endl; // } // #pragma omp for // for (int j = 666; j < 1000; j++) { // cout << "-" << endl; // } // } // // 可知,barrier爲隱式柵障,即並行區域中全部線程執行完畢以後,主線程才繼續執行。 // 而nowait的聲明便可取消柵障,這樣,即便並行區域內即便全部的線程尚未執行完, // 可是執行完了的線程也沒必要等待全部線程執行結束,而可自動向下執行。 // 以下所示正常來講應該是第一個for循環中的一個線程執行完以後nowait進入下一個for循環, // 可是咱們經過 #pragma omp barrier 來做爲顯示同步柵障,即讓這個先執行完的線程等待全部線程執行完畢再進行下面的運算 // #pragma omp parallel // { // #pragma omp for nowait // for (int i = 0; i < 100; i++) { // cout << "+" << endl; // } // #pragma omp barrier // #pragma omp for // for (int j = 666; j < 1000; j++) { // cout << "-" << endl; // } // } // 這裏咱們經過#pragma omp master來讓主線程執行for循環,而後其餘的線程執行後面的cout語句, // 因此,cout的內容會出如今for循環屢次(這取決於你電腦的性能),最後,主線程執行完for語句後,也會執行一次cout // #pragma omp parallel // { // #pragma omp master // { // for (int i = 0; i < 10; i++) { // cout << i << endl; // } // } // cout << "This will be shown two or more times" << endl; // } // 使用section能夠指定不一樣的線程來執行不一樣的部分 // 以下所示,經過#pragma omp parallel sections來指定不一樣的section由不一樣的線程執行 // 最後獲得的結果是多個for循環是混雜在一塊兒的 // #pragma omp parallel sections // { // #pragma omp section // for (int i = 0; i < 10; i++) { // cout << "+"; // } // #pragma omp section // for (int j = 0; j < 10; j++) { // cout << "-"; // } // #pragma omp section // for (int k = 0; k < 10; k++) { // cout << "*"; // } // } system("pause"); return 0; }
經過上面的例子,咱們就能夠對OpenMP有一個基本的入門過程了。