使用OpenMP需要在編譯器上打開OpenMP開關,並包含omp.h文件。我使用的是在Windows下的Visual Studio 2015,只需在工程選項中打開OpenMP支持就可以了。按照書上的說法,GCC增加參數-fopenmp就可以了。
OpenMP有兩個重要的函數:
omp_get_thread_num()
omp_get_num_threads()
他們的返回值都是無符號整數,第一個用來返回當前執行的線程編號,而第二個返回一共有的線程數量。和C語言的數組下標類似,當前執行的線程編號的值從0開始,最大值總是線程數量減一。至於這兩個函數的用途,是進行線程的任務分配。
#pragma omp為OpenMP的預處理器指令,他將告訴編譯器的OpenMP的具體行為。
在5.1節中主要涉及到了OpenMP的#pragma omp parallel指令,用來將當前指令並行化。注意這個指令並不包括任何的任務分配,只是簡單的將他下面的指令送給不同線程去執行而已。所以,簡單的一條#pragma omp parallel指令下面接一個for循環,而for循環的起止點都是常數而不是根據當前線程分配的,只會把這個for循環用線程數量執行一遍,與我們預期的“將內容拆開,分配給不同線程”的想法相悖,無法起到並行化的效果。
書后面提到#pragma omp parallel for就可以自動分配線程任務,但是具體的分配方式還是取決於具體的控制語句。
對於OpenMP指令,還可以跟上一些從句(clause)。比如,在上面的#pragma omp parallel,可以跟上num_threads(thread_count),表示用thread_count的線程數量來運行。除此之外還有一條#pragma omp critical指令,用來在不同線程訪問或者更新所有線程共享的資源(比如全局變量),用來防止競爭的情況(race condition)。
書上還說了一些概念性的東西:不同的線程是從進程forked出來的,共享大部分進程的資源(比如stdin,stdout,等),但是不同的線程享有不同的棧(stack)空間和程序計數器(program counter)。當一個線程執行完畢,將返回進程(的主線程)。不同的棧空間意味着局部變量是獨立的,盡管可能是使用了同一個函數,但是他們的局部變量在OpenMP創建了子線程后依然是相互獨立不相干的。
例題:用OpenMP實現定義法計算曲邊梯形(定積分幾何意義)的並行算法。
定義法計算定積分包括分割,求和,取極限三個步驟。串行算法比較容易實現,用循環累加分割的小區間的值就可以了。並行的時候,因為各個小區間求和互不相干(沒有依賴性(dependence)和相互通信(communication)),因此只需要將不同小區間分配給不同線程就可以了。最后#pragma omp critical指令強調了匯總操作時只能由一個線程更新其共享資源(也相當於鎖)。
例子的完整的實現如下:
#include<stdio.h> #include<time.h> /* clock() function */ #include<stdlib.h> #include<omp.h> /* OpenMP header */ #include<math.h>
double result_parallel; #define N 400000000
double get_approx_serial(double(*func)(double x), double interval_a, double interval_b, long long int n) { double h; double result = 0; double x_i; int i; h = (interval_b - interval_a) / n; for (i = 0; i < n; i++) { x_i = interval_a + h*i; result += func(x_i)*h; } return result; } void get_approx_parallel(double(*func)(double x), double interval_a, double interval_b, long long int n) { double h; double result = 0; double x_i; long long int local_i_start, local_i_end; int thread_id, total_threads; int i; /* Get thread num */ total_threads = omp_get_num_threads(); thread_id = omp_get_thread_num(); /* Task Arrangment between threads */ local_i_start = (n / total_threads)*thread_id; if (thread_id == total_threads - 1) local_i_end = n; else local_i_end = (n / total_threads)*(thread_id + 1); /* approx Algorithm */ h = (interval_b - interval_a) / n; for (i = local_i_start; i < local_i_end; i++) { x_i = interval_a + h*i; result += func(x_i)*h; } #pragma omp critical result_parallel += result; return; } int main() { clock_t start_clock, end_clock; int total_thread_num; /* Get total threads to display */
#pragma omp parallel total_thread_num = omp_get_num_threads(); printf("This computer Has %d Threads in total\n", total_thread_num); /* Do serial algorithm and get the used time. */ start_clock = clock(); printf("The area between y=sin(x) , y=0 , x=0 ,x=pi is %lf\n", get_approx_serial(&sin, 0, 3.141593, N)); end_clock = clock(); printf("Serial total use Time %d ms", end_clock - start_clock); /* Do parallel algorithm and get the used time. */ start_clock = clock(); #pragma omp parallel get_approx_parallel(&sin, 0, 3.141593, N); printf("\nThe area between y=sin(x) , y=0 , x=0 ,x=pi is %lf\n", result_parallel); end_clock = clock(); printf("Parallel total use Time %d ms", end_clock - start_clock); system("pause"); return 0; }
critical語句的反匯編情況如下,證明了編譯器在解釋這條語句時轉換成了兩個函數調用,來處理線程的競爭沖突。
程序的運行結果如下所示:我的機器是6核12線程的,所以一共有12條線程可以使用,並行算法比串行算法實際加速比(Speedup)$S=T_s/T_p=9912/984=10.07$,效率(efficiency)$E=(1/p)S=0.839$(計算方法和定義見書58頁)。根據阿姆達爾定律(Amdahl's law) ,隨着數據規模$N$的增大,串行代碼占比幾乎為零(只有#pragma omp critical子句聲明的為串行代碼),這個算法可以達到理論上幾乎線性的加速比。實際上因為SMT超線程效率有限,加速比為並未達到12。
參考書籍:並行程序設計導論(英文版),機械工業出版社。