HPC学习(二)
文章目錄
- OMP學習
- introduction
- 環境配置
- OpenMP API
- 編譯器指令
- 庫函數
- 互斥鎖
- 數據依賴與沖突
- 原子操作與鎖
- 原子操作理解
- 原子操作與鎖的區別
- OpenMP實操(多線程矩陣乘法)
OMP學習
motivation:剛接觸高性能運算,學習方向有點亂,偶然在網上找到一篇博客: ASC18華農隊長超算競賽完整備戰指南.,決定按照這個思路進行整理學習。本篇博客主要用于本人學習記錄,如有錯誤,歡迎各位大佬指出。(大佬勿噴…)
這篇繼續按照指南順序,學習openmp編程。
學omp推薦b站網課:新竹清華大學并行計算與并行編程課程,挑選著看,感覺不需要太多基礎都可以看懂。
introduction
OpenMP = open specification for multi-processing
OpenMP是由一組計算機硬件和軟件供應商聯合定義的應用程序接口(API);
OpenMP為基于共享內存的并行程序的開發人員提供了一種便攜式和可擴展的編程模型,其API支持各種架構上的C/C++和Fortran;
omp的并行模型稱為fork-join模型
由主線程創造出多個線程(fork過程),
并行代碼執行完后,只剩下一個主線程(join過程)
環境配置
OpenMP API
關于OpenMp的API介紹,有一位博主已經寫得很全面了,想要深入學習的話去看看。以下內容參考了下方博客并根據我自己的需要簡化,會相對簡略。(博客鏈接:OpenMP學習筆記)
OpenMP API總覽
編譯器指令(44個)
運行時庫函數(35個)
環境變量(13條個)
編譯器指令
編譯器指令在你的源代碼中可能被顯示為注釋,并且被編譯器所忽略,除非你明顯地告訴編譯器
OpenMP的編譯器指令的目標主要有:
| parallel | 用在一個結構塊之前,表示這段代碼將被多個線程并行執行 |
| for | 用于for循環語句之前,表示將循環計算任務分配到多個線程中并行執行,以實現任務分擔。注意要保證每次循環之間無數據相關性。 |
| parallel for | parallel和for指令的結合,也是用在for循環語句之前,表示for循環體的代碼將被多個線程并行執行,它同時具有并行域的產生和任務分擔兩個功能 |
| sections | 用在可被并行執行的代碼段之前,用于實現多個結構塊語句的任務分擔,可并行執行的代碼段各自用section指令標出(注意區分sections和section) |
| parallel sections | parallel和sections兩個語句的結合,類似于parallel for |
| single | 用在并行域內,表示一段只被單個線程執行的代碼 |
| private | 子句可以將變量聲明為線程私有,每個線程都有一個該變量的副本,不會互相影響。原變量在并行部分不起任何作用,也不會受到并行部分內部操作的影響。 |
| firstprivate | 在private的基礎上,用原變量的值初始化線程中的副本 |
| lastprivate | 在并行部分結束時,將程序語法上最后一次迭代的值賦回給變量 |
| threadprivate | 只針對全局變量,使得每個線程都有一個私有的全局對象 |
| shared | shared可以將一個變量聲明為共享變量,在多個線程中共享,但要注意使用安全。 |
| reduction | reduction子句可以對一個或者多個參數指定一個操作符,然后每一個線程都會創建這個參數的私有拷貝,在并行區域結束后,迭代運行指定的運算符,并更新原參數的值。eg:reduction(+: sum) |
| copyin | copyin子句可以將主線程中變量的值拷貝到各個線程的私有變量中,讓各個線程可以訪問主線程中的變量。copyin的參數必須要被聲明稱threadprivate. |
| schedule(method=static, size=1) | 線程分配方式,method參數有static(平均分配,線程間迭代數最多相差size),dynamic(根據程序執行動態分配),size表示每次分配的迭代樹最少是多少。 |
庫函數
接下來介紹部分常用的庫函數
| int omp_get_num_procs(void) | 返回調用函數時可用的處理器數目 |
| int omp_get_num_threads(void) | 返回當前并行區域中的活動線程個數 |
| int omp_get_thread_num(void) | 返回當前線程號 |
| int omp_set_num_threads(void) | 設置進入并行區域時,將要創建的線程個數 |
| int omp_get_max_threads() | 返回最大線程數量,可用上一個函數修改 |
| int omp_in_parallel() | 可以判斷當前是否處于并行狀態,返回0或1 |
| void omp_set_dynamic(int) | 該函數可以設置是否允許在運行時動態調整并行區域的線程數。0表示禁用,其他數字表示可用。 |
| int omp_get_dynamic() | 返回當前程序是否允許在運行時動態調整并行區域的線程數。 |
互斥鎖
與互斥鎖相關的函數我獨立開一個表格來介紹
| void omp_init_lock(omp_lock) | 初始化互斥鎖 |
| void omp_destroy_lock(omp_lock) | 銷毀互斥鎖 |
| void omp_set_lock(omp_lock) | 獲得互斥鎖 |
| void omp_unset_lock(omp_lock) | 釋放互斥鎖 |
| bool omp_test_lock(omp_lock) | 該函數可以看作是omp_set_lock的非阻塞版本。 |
補充一下omp_lock變量。
定義: omp_lock_t lock; 使用: omp_init_lock(&lock)也就是說,omp_lock是一個omp_lock_t類型變量的指針
數據依賴與沖突
關于這方面的內容國內網站上好像比較少也比較散亂,我就按自己的理解說一下(水平有限,不敢保證正確性…)
什么叫數據依賴(carries dependency)?舉個栗子就能理解:
c = a + b;
e = c + d;
c 數據依賴于 e,因為它需要 c 的值。
(應該沒理解錯吧…)
數據沖突
在多線程編程中,當各個線程對某一個變量同時進行讀取修改的時候,線程讀取的順序就會對結果造成影響,這時候就會發生數據沖突。(個人淺見)
原子操作與鎖
原子操作理解
首先要理解什么是原子操作。下面的解釋是從原子操作這篇博客中截取的。
原子操作(atomic operation)指的是由多步操作組成的一個操作。如果該操作不能原子地執行,則要么執行完所有步驟,要么一步也不執行,不可能只執行所有步驟的一個子集。
現代操作系統中,一般都提供了原子操作來實現一些同步操作,所謂原子操作,也就是一個獨立而不可分割的操作。在單核環境中,一般的意義下原子操作中線程不會被切換,線程切換要么在原子操作之前,要么在原子操作完成之后。更廣泛的意義下原子操作是指一系列必須整體完成的操作步驟,如果任何一步操作沒有完成,那么所有完成的步驟都必須回滾,這樣就可以保證要么所有操作步驟都未完成,要么所有操作步驟都被完成。
例如在單核系統里,單個的機器指令可以看成是原子操作(如果有編譯器優化、亂序執行等情況除外);在多核系統中,單個的機器指令就不是原子操作,因為多核系統里是多指令流并行運行的,一個核在執行一個指令時,其他核同時執行的指令有可能操作同一塊內存區域,從而出現數據競爭現象。多核系統中的原子操作通常使用內存柵障(memory barrier)來實現,即一個CPU核在執行原子操作時,其他CPU核必須停止對內存操作或者不對指定的內存進行操作,這樣才能避免數據競爭問題。
說實話,在閱讀完上述解釋后,我還是有些云里霧里的,我再找了一個簡單的程序,也許能幫助理解。
#include<iostream> #include<omp.h> #include<time.h> #include<Windows.h> using namespace std;int main(int argc, char** argv) {omp_set_num_threads(6);int counter = 0, i; #pragma omp parallel{for (i = 0; i < 10; i++){ #pragma omp atomiccounter++;}}cout << "counter =" << counter;return 0; }一共6個線程,因此理論輸出為6*10=60,符合實際結果。
原子操作與鎖的區別
一般而言,使用原子操作能得到更好的性能。
OpenMP實操(多線程矩陣乘法)
//矩陣乘法 #include<iostream> #include<omp.h> #include<time.h> #include<Windows.h> using namespace std;//int main(int argc, char** argv) //{ // omp_set_num_threads(6); // int counter = 0, i; //#pragma omp parallel // { // for (i = 0; i < 10; i++) // { //#pragma omp atomic // counter++; // } // } // cout << "counter =" << counter; // return 0; // // //}#define Max 10 #define Min -10 #define Size 500//隨機數生成 double getrandom(int max = Max, int min = Min) {double tmp1 = double(rand() % 101) / 101;double tmp2 = (double)((rand() % (max - min)) + min);return tmp1+tmp2; }//生成指定大小的隨機方陣 void getmatrix(double* Matrix,int size = Size) {for (int i = 0; i < size; i++)for (int j = 0; j < size; j++)Matrix[i * size + j] = getrandom(); }//微秒級別的windows平臺計時函數 ////傳統方法 double* baseline(double* A, double* B, int size = Size) {double* result;result = new double[size * size];for(int i = 0; i < size; i++)for (int j = 0; j < size; j++) {result[i * size + j] = 0;for (int k = 0; k < size; k++)result[i * size + j] += A[i * size + k] * B[k * size + j];}return result; }//多線程版本1 double* Multithread(double* A, double* B, int size = Size) {double* result;result = new double[size * size];int i, j, k;#pragma omp parallel for schedule(dynamic) firstprivate(i,j,k)for (i = 0; i < size; i++) //#pragma omp forfor (j = 0; j < size; j++) {result[i * size + j] = 0; //#pragma omp parallel for num_threads(3)for (k = 0; k < size; k++)result[i * size + j] += A[i * size + k] * B[k * size + j];}return result; }int main(int argc, char** argv) {double* A, * B, * C;int n = Size;srand((unsigned)time(NULL));//分配動態空間A = new double[n * n];B = new double[n * n];C = new double[n * n];getmatrix(A);getmatrix(B);//初始化結束,準備開始計時LARGE_INTEGER large_interger; //記錄頻率和周期的結構體double dff; //頻率__int64 c1, c2; //周期QueryPerformanceFrequency(&large_interger);dff = large_interger.QuadPart;QueryPerformanceCounter(&large_interger);c1 = large_interger.QuadPart;C = baseline(A, B);QueryPerformanceCounter(&large_interger);c2 = large_interger.QuadPart;cout << "baseline運行時間為(單位:毫秒):" << (c2 - c1) * 1000 / dff << '\n';cout << C[50, 50] << '\n';QueryPerformanceFrequency(&large_interger);dff = large_interger.QuadPart;QueryPerformanceCounter(&large_interger);c1 = large_interger.QuadPart;C = Multithread(A, B);QueryPerformanceCounter(&large_interger);c2 = large_interger.QuadPart;cout << "多線程矩陣乘法1運行時間為(單位:毫秒):" << (c2 - c1) * 1000 / dff << '\n';cout << C[50, 50] << '\n';delete [] A;delete [] B;delete [] C;return 0; }代碼部分會有幾個警告,還不太清楚要如何處理,但似乎對結果沒有什么影響。
事實上,通過修改循環順序以及使用矩陣分塊能夠進一步提升代碼性能,但我是懶得寫了,,,有興趣的可以參考“程序性能優化探討(6)——矩陣乘法優化之分塊矩陣”,這篇寫的非常詳細。
openmp暫時就寫那么多,以后有想法會繼續補充。
下一篇應該會開始寫MPI。
總結
- 上一篇: java实现文本编辑器
- 下一篇: SwiftyJSON源码分析