OpenMP的簡單使用教程

OpenMP的簡單使用教程
今天有幸參加了一個XSEDE OpenMP的workshop講座,真是受益匪淺啊。簡單來說OpenMP就是一個多線程程序的框架。和MPI相比,MPI每一個Node都有獨立的內存空間,但是OpenMP所有的線程共享一個內存空間。顯而易見,OpenMP的硬件制約要比MPI大,但是隻要硬件跟得上就會比MPI要快。OpenMP一般都會部署再超級計算機中心,但是幾年之前它就成爲了一個通用標準。基本上所有的主流C/C++語言編譯器都支持OpenMP(當然除了C之外,OpenMP還支持Fortran,不過這裏我主要介紹一下C),這意味着只要你的計算機上安裝了C的編譯器你就可以直接使用OpenMP不需要額外部署任何東西。(這太方便了,想想Hadoop,當年爲了部署它,我被折磨的那個叫一個銷魂啊。)

##編寫OpenMP程序

編寫OpenMP的程序並不需要額外的學習很多東西,其實就是普通的C代碼加上一些Directives。用Hello World爲例:

#include<stdio.h>
int main(int argc,char** argv){
  printf("Hello World!\n");
  return 0;
}

這是一個最簡單的程序,編譯執行後的輸出是。

Hello World!
然後我們給他加上OpenMP的directive,他就變成了。

#include<stdio.h>
int main(int argc,char** argv){
  #pragma omp parallel
  printf("Hello World!\n");
  return 0;
}

看到沒?就是簡單的加了一句話#pragma omp parallel,若是正常編譯的話,這句話會被忽略一點都不影響你的程序,只有調用OpenMP的lib編譯的時候纔會編譯成OpenMP的版本。以GCC爲例,OpenMP的編譯方法是:

gcc -o hello hello.c -fopenmp
僅僅多了一個-fopenmp的flag,太簡單了。現在我們試試效果,這個hello world的輸出結果變成了:

Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!

輸出了8次Hello World!。這是爲什麼呢?原因是那個directive之後的代碼被多線程操作了,默認情況下GCC的-fopenmpflag會調用和你CPU內核數相同數量的線程來執行程序。這個線程數量是可以控制,只需要修改環境變量中OMP_NUM_THREADS參數,例如:

export OMP_NUM_THREADS=2
之後也不需要重新編譯,直接執行之前的程序,就會發現Hello World!的數量變成了兩個了。

###for循環

大多數情況下,我們主要會將多線程技術應用在循環中而不是全部代碼。OpenMP主要被應用於for循環的多線程處理,這主要還是因爲for循環比較容易控制。當然如果你非要用在while循環上也不是不可以,只不過要大量修改你的代碼然後用一個block來圈在while之外,總之是一個比較另類的操作了。我在此就不多說了。用一個最簡單的例子,找尋1到10000中最大的數字。當然這個例子很白癡,但是代碼簡潔比較好理解。

#include<stdio.h>
int main(int argc, char** argv){
  int i;
  int max = 0;
  #pragma omp parallel for
  for(i=0;i<=10000;i++){
    if(i>max)max=i;
  }
  printf("%d\n",max);
}

結果是:

10000
我們在directive裏面加了一個for,變成了#pragma omp parallel for,這樣的話OpenMP就只會把下面的for循環進行多線程處理,所以我們只看到了一個輸出而不是好幾個。

這裏有一點一定要主要,將要進行多線程處理的for循環一定是獨立的(independent),也就是說下面這種情況是不可以的。

for(i=0;i<10000;i++){
  a[i] = a[i-1]+1;
}

每一次循環都需要之前的結果,這種循環沒有辦法進行多線程處理,因爲每一次都要等待之前的輸出,強行處理還會出錯。

####private參數

細心的話,也許你會有一個問題。那就是循環只有一個迭代器(通常是變量i),但進行多線程處理的時候,這個迭代器會不會被各個線程互相扯皮?這卻是是一個問題,如果這個迭代器僅僅用作計數的話可能還不是什麼大問題,但是如果這個變量也參與運算,這就麻煩了,所有OpenMP引入了private參數,用來告訴編譯器那些變量需要有一個本地的實例。這個參數用於迭代器的話就變成了下面的例子。

#pragma omp parallel for private(i)
for(i=0;i<10000;i++){
  ...
}

這樣的話每個線程都有自己的i拷貝,就不會衝突了。當然這個參數的用途很廣,這僅僅是一個簡單的例子。但事實上基本上每次對循環進行多線程處理的時候都需要拷貝迭代器,因此可以把for private()這樣連起來記憶,不容易忘。

####reduction參數

我們回到之前的那個10000以內最大整數的例子。之前我提到了循環一定不能互相關聯,否則不是效率低下(還不如單線程),就是出錯誤。這個例子其實就是一個反面典型,就是因爲max這個變量。循環的每一步都會讀取之前的結果來參與計算。可是針對max變量的這個例子,我們還是有解決辦法的。

如果我們環境變量設置線程數爲2,這個循環的前5000項和後5000項將分別在兩個不同的線程中處理,也就是一份爲二。我們需要的是所有數值中的最大值,換一個角度想。我們可以在前5000項和後5000項分別算出最大值,然後在對這兩個結果進行比較取最大值,這樣的話我們同樣完成了尋找最大值的目的同時還可以多線程處理。

那麼怎樣做到呢?這個時候我們就需要reduction這個參數。reduction就是讓某些變量先在各自的線程中獨自計算,然後在循環結束時在合併。那麼我們用這個參數來修改之前的例子:

#include<stdio.h>
int main(int argc, char** argv){
  int i;
  int max = 0;
  #pragma omp parallel for private(i) reduction(max:max)
  for(i=0;i<=10000;i++){
    if(i>max)max=i;
  }
  printf("%d\n",max);
}

這下就變成了完整版。reduction這個函數格式是reduction(operation:variable),冒號前面的是操作類型,冒號後面的是變量名。目前reduction這個函數只支持如下幾個操作:

+(初始值是0)
-(初始值是0)
max(初始值是最小值)
min(初始值是最大值)
Bit(&,|,^,iand,ior)(初始值是~0,0)
Logical(&&,||,.and.,.or.)(初始值是0,1,.true.,.false.)
##編譯與執行

其實之前已經提到了如何編譯和執行。今天有幸在超計算機中心的服務器上面測試了幾次,然後回到本地計算機試了一下,發現本地執行簡單的多。因爲本地執行就是簡單的./program。Windows下的話你可以試試雙擊。在服務器上面跑還要考慮調度多少node和多少core,但是在本地不需要提供任何額外的參數就和執行普通程序一樣。所以說OpenMP真是多線程計算一大神器啊,主要還是操作簡單。

之前提到了現在主流的C/C++編譯器都已經支持OpenMP了,那麼都有那些編譯器呢?我在這裏給出一個列表。

編譯器 參數 不設置環境變量時的初始值
GNU (gcc, g++, gfortran) -fopenmp 與CPU內核數相同數量的線程
Intel (icc ifort) -openmp 與CPU內核數相同數量的線程
Portland Group (pgcc,pgCC,pgf77,pgf90) -mp 只使用一個線程
順便在提一下,環境變量是控制線程數的環境變量是OMP_NUM_THREADS。

###參考文獻

XSEDE HPC Workshop: Open MP
How to compile and run openMP program
注:轉載僅作爲筆記使用,如有侵權,請聯繫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章