OpenMP設定

  • 要將 for 迴圈平行化處理,該怎麼做呢?非常簡單,只要在前面加上一行 #pragma omp parallel for 即可。
    • 編譯命令 gcc -o mp_hello -fopenmp mp_hello.c
    • pragma omp parallel 僅在指定了 -fopenmp 編譯器選項後才會發揮作用,詳見GCC文件

第一個範例

// mp_hello.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

void Test( int n ) {
    for( int i = 0; i < 10000; ++ i ) {
        // do nothing, just waste time
    }
    printf( "%d, ", n );
}

int main(int argc, char* argv[]) {
    #pragma omp parallel for
    for( int i = 0; i < 10; ++ i )
        Test( i );                                                   
    return EXIT_SUCCESS;
}

可得到以下結果:

9, 0, 4, 7, 1, 2, 5, 6, 3, 8
  • 可以從結果很明顯的發現,他沒有照著 0 到 9 的順序跑了!而上面的順序怎麼來的?其實很簡單,OpenMP 只是把迴圈 0 – 9 共十個步驟,丟給不同的執行緒去跑,所以數字才會出現這樣交錯性的輸出。

  • OpenMP語法 #pragma omp directive [clause],而本例中的parallel與for都是directives,因此

#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 );
}
  • OpenMP的directives如下:

    • atomic : 記憶體位址將會自動更新。這個指令的目的在於避免變數被同時修改而造成計算結果錯誤
    • barrier : 等待,直到所有的執行緒都執行到 barrier。用來同步化。
    • critical : 強制接下來的程式一次只會被一個執行緒執行。
    • flush : Specifies that all threads have the same view of memory for all shared objects.
    • for : 用在 for 迴圈之前,會將迴圈平行化處理。(註:迴圈的 index 只能是 int)
    • master : 指定由主執行緒來執行接下來的程式。
    • ordered : 指定接下來被程式,在被平行化的 for 迴圈將依序的執行。
    • parallel : 代表接下來的程式將被平行化。
    • section : 將接下來的 section 平行化處理。
    • single : 之後的程式將只會在一個執行緒執行,不會被平行化。
    • threadprivate : Specifies that a variable is private to a thread.
  • 而 clause 的部份,則有如下:

    • copyin : 讓 threadprivate 的變數的值和主執行緒的值相同。
    • copyprivate : 將不同執行緒中的變數共用。
    • default : 設定平行化時對變數處理方式的預設值。
    • firstprivate : 讓每個執行緒中,都有一份變數的複本,以免互相干擾;而起始值則會是開始平行化之前的變數值。
    • if : 判斷條件,可以用來決定是否要平行化。
    • lastprivate : 讓每個執行緒中,都有一份變數的複本,以免互相干擾;而在所有平行化的執行緒都結束後,會把最後的值,寫回主執行緒。
    • nowait : 忽略 barrier(等待)。
    • num_threads : 設定平行化時執行緒的數量。
    • ordered : 使用於 for,可以在將迴圈平行化的時候,將程式中有標記 directive ordered 的部份依序執行。
    • private : 讓每個執行緒中,都有一份變數的複本,以免互相干擾。
    • reduction : 對各執行緒的變數,直行指定的運算元來合併寫回主執行緒。
    • reduction : Specifies that one or more variables that are private to each thread are the subject of a reduction operation at the end of the parallel region.
    • schedule : 設定 for 迴圈的平行化方法;有 dynamic、guided、runtime、static 四種方法。
    • shared : 將變數設定為各執行緒共用(應該算是相對於 private 的)。

parallel directives

  • parrallel 的語法很直接,就是 #pragma omp parallel;不過原則上,後面要用 { } 來指定 scope。範例程式如下
// mp_parallel.c
#include <omp.h>
#include <stdio.h>  
#include <stdlib.h>

void Test( int n ) {
    printf( "<T:%d> - %d\n", omp_get_thread_num(), n );
}

int main(int argc, char* argv[]) {
    // 不指定thread個數,預設使用全部的threads
    #pragma omp parallel       
    {
        Test( 0 );
    }
    return EXIT_SUCCESS;
}
  • 編譯 gcc -o mp_parallel -fopenmp mp_parallel.c 再執行。
  • 從結果可以看出來,Test() 被不同的 thread 個別執行了一次,所以會輸出多行;這是因為 OpenMP 會根據硬體,自動選擇預設的執行緒數目。

  • 接著,在針對程式些修改,指定thread個數

    #include <omp.h>
    #include <stdio.h>
    #include <stdlib.h>
    #define OMP 8

    void Test( int n ) {
        printf( "<T:%d> - %d\n", omp_get_thread_num(), n );
    }

    int main(int argc, char* argv[]) {
        // 用if else限制thread個數
        #pragma omp parallel if (OMP > 4) num_threads (3)
        {
            Test( 0 );
        }
        return EXIT_SUCCESS;
    }
  • 在程式中,加入了 if 和 num_threads() 這兩個語法。num_threads() 是用來指定執行緒的數目的,而在上面的程式中,把它指定成 3,所以結果會由三個不同的 thread,個別呼叫一次 Test()。

single, master directives

  • 而在 parallel 的範圍內,還有一些 directive 是可以使用的;像是 single、master 等等。
// mp_parallel3.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#define OMP 8

void Test( int n ) {
      printf( "<T:%d> - %d\n", omp_get_thread_num(), n );
}

int main(int argc, char* argv[]) {
      #pragma omp parallel  num_threads (4)
      {
          for (int i = 0; i < 2; ++i)
              Test(i);
          printf("end of for\n");

          # pragma omp single
          {
              printf("single region\n");
          }

          # pragma omp master
          {
              printf(" master region\n");
          }
      }
      return EXIT_SUCCESS;
  • 執行結果如下:
<T:3> - 0
<T:3> - 1
end of for
<T:0> - 0
<T:0> - 1
end of for
<T:1> - 0
<T:1> - 1
end of for
<T:2> - 0
<T:2> - 1
end of for
single region
master region
  • 可以發現加上 single 和 master 的部份的程式只會被執行一次;
  • 而 master 和 single 兩者間的差異,則是 master 會一定由主執行緒來執行,single 不一定。

section directives

  • section 的用處,是把程式中沒有相關性的各個程式利用 #pragma omp section 來做區塊切割,然後由 OpenMP 做平行的處理。
    int main(int argc, char* argv[])
    {
      #pragma omp parallel sections
      {
         #pragma omp section
         {
             for( int k = 0; k < 100000; ++ k )
                 {}
             Test( 0 );
         }
         #pragma omp section
         {
             Test( 1 );
         }
         #pragma omp section
         {
              Test( 2 );
         }
         #pragma omp section
         {
              Test( 3 );
         }
     }
    }
    
  • 執行結果如下:

    <T:1> - 1
    <T:1> - 2
    <T:1> - 3
    <T:0> - 0
    
  • 從執行的輸出結果可以發現:由於 thread 0 先執行了執行時間最久的第一個 section,而在 thread 0 結束第一個 section 前,其他三個 section 已經由 thread 1 執行結束了。

  • 不過利用 sections 平行化的時候,要注意程式的相依性;如果兩段程式是有相關性的話,實際上並不適合用 sections 來做平行化。

ordered directives

  • ordered 的部份,則是 directive 和 clauses 同時使用
    #pragma omp parallel for ordered
    for( int i = 0; i < 6; ++ i )
    {
     #pragma omp ordered
    Test( i );
    }
    
  • 執行結果:

    <T:0> - 0
    <T:0> - 1
    <T:0> - 2
    <T:1> - 3
    <T:1> - 4
    <T:1> - 5
    
  • 而如果將裡兩項 ordered 其中一項拿掉,都不會有 ordered 的效果。不過值得注意的一點是:本來在平行化後,輸出結果顯示的執行緒編號是 0 和 1 交錯出現,而現在則變成 thread 0 跑完後,再跑 thread 1 了。而在實際運算的時間上,加了 ordered 後,,和沒有平行化之前相差不大的時間。

  • 此外,#pragma omp ordered 的功用,應該是將這個被平行化的 for 迴圈,從執行到 ordered directive 這一刻開始,之後的程式都會依序執行。比如說將上面的程式改為

    #pragma omp parallel for ordered
    for( int i = 0; i < 6; ++ i )
    {
      Test( i );
      #pragma omp ordered
      Test( 10 + i );
    }
    
  • 結果如下:

    <T:0> - 0
    <T:1> - 3
    <T:0> - 10
    <T:0> - 1
    <T:0> - 11
    <T:0> - 2
    <T:0> - 12
    <T:1> - 13
    <T:1> - 4
    <T:1> - 14
    <T:1> - 5
    <T:1> - 15
    
  • 可以很清楚的看到,除了輸出結果的第二行外,其他所有的輸出結果,都是依照順序的;這是因為當程式執行到 ordered directive 的時候,已經跑完了 Test(0) 和 Test(3),所以才會使的他們不是依照順序。此外,他也沒辦法讓迴圈裡的第二次 Test() 都依照順序、Test() 不依照順序。

變數的平行化

  • 在將程式平行化的時候,其實還可能碰到一些問題。其中一個最大、最有可能碰到的,就是平行化後,每個執行緒裡的變數的獨立與否。下面是一個簡單的兩層迴圈的程式: ```c void Test2( int n, int m ){ printf( " - %d, %dn", omp_get_thread_num(), n, m ); }

pragma omp parallel for

for( int i = 0; i < 3; ++ i ) for( int j = 0; j < 3; ++ j ) Test2( i, j );

* 執行結果如下,完整的執行9次。

- 0, 0

- 2, 0

- 0, 1

- 2, 1

- 0, 2

- 2, 2

- 1, 0

- 1, 1

- 1, 2


* 但是如果把程式改成如下:
```c
int i,  j;
#pragma omp parallel for
for( i = 0; i < 3; ++ i )
    for( j = 0; j < 3; ++ j )
            Test2( i, j );
  • 執行結果會錯誤如下,最直接的問題,3 x 3 迴圈應該要跑九次 Test(),但是他只跑了 7 次。原因就是 OpenMP 會把在 parallel 的範圍以外宣告的變數,當成是所有執行緒共用的;所以在執行的時候,兩個執行緒可能會同時修改到相同的j (race condition),導致迴圈執行的次數比預期的少。
<T:0> - 0, 0
<T:1> - 2, 0
<T:0> - 0, 1
<T:1> - 2, 2
<T:0> - 1, 0
<T:1> - 2, 1
<T:0> - 1, 2
  • 而解決的方法,就是透過 OpenMP 的 private,來讓每個執行緒對變數 j 有各自的副本;寫法如下:
    int i,  j;
    #pragma omp parallel for private( j )
    for( i = 0; i < 3; ++ i )
      for( j = 0; j < 3; ++ j )
              Test2( i, j );
    
  • 同樣的情形,也會發生在使用 sections 的時候,所以在使用 OpenMP 平行化的時候,要注意有沒有將平行化範圍外的變數拿來在各個不同的執行緒使用。
  • 而相對於 private,OpenMP 有另一個 clause 是 shared,他是用來讓所有執行緒共用變數的語法;不過在一般時候,應該是不需要特別去指定 shared,因為預設值就已經是了。

  • 而在 private 和 share 的設定,OpenMP 還有提供一個 clause 叫做 default(詳見 MSDN)。他的功用,就是指定預設的範圍外變數配置方法;值可以是 shared 或 none。OpenMP 的預設值就是 shared,在沒有修改或另外指定的情況下,所有的範圍外變數都會以共享的方式來配置。而如果指定成 none 的話,則必需替所有範圍外變數指定配置的方法,否則在編譯的時候就會失敗。

int X;
#pragma omp parallel default(none)
{
    #pragma omp for
        for( int i = 0; i < 4; ++ i )
            for( X = 0; X < 4; ++ X )
                Test2( i, X );
}
  • 由於程式中的 X 是在 #pragma omp parallel 的範圍外,而又指定了 default(none),所以在沒有指定變數 X 的情況下,編譯器會出現錯誤訊息。這樣的好處就是可以避免應該指定成 private 卻沒有指定的情形;缺點就是,要額外指定 shared。而修改成下面的形式,就可以編譯、執行了。
int X;
#pragma omp parallel default(none)
{
    #pragma omp for private( X )
        for( int i = 0; i < 2; ++ i )
            for( X = 0; X < 2; ++ X )
               Test2( i, X );
}
  • 而除了 private 外,還有兩個類似的用法,分別是 firstprivate 和 lastprivate。其中,firstprivate除了有 private 的功能外,還會將各執行緒的 private 變數,設定為執行緒開始前的變數值(透過 copy constructor);而 lastprivate則是會將變數在執行緒最後的值,寫回主執行緒(透過 assignment constructor)。

  • 還有一點要注意的,就是在使用 shared 的變數的時候,如果有可能會有「多個執行緒同時修改同一個變數」的情形發生,那可能會讓程式結果有問題!(race conditions)。

    int sum = 0;
    #pragma omp parallel
    {
      #pragma omp for
      for( int i = 0; i < 10000; ++ i )
          for( int j = 0; j < 50000; ++ j )
              sum += x;
    }
    printf( "%dn",sum );
    
  • 很直接的想法,結果應該會是 10,000 * 50,000 = 500,000,000 吧?但實際上每次得到的結果均不相同 ;這就是因為同時修改變數 sum 所造成的結果。而要避免這種情況,可以使用 atomic 這個 directive。

    int sum = 0;
    #pragma omp parallel
    {
      #pragma omp for
      for( int i = 0; i < 10000; ++ i )
          for( int j = 0; j < 50000; ++ j )
              #pragma omp atomic
              sum += x;
    }
    printf( "%dn",sum );
    
  • 而這樣執行的結果,就會是正確的值了~不過相對起來,為了避免同時修改的問題,執行上的速度也慢了不少;本來的程式只要 6000ms 左右就可以執行完成,而加入了 atomic 後,卻需要用到 17000ms 左右的時間。此外,atomic 也不是在所有情形都能用,限制也相當的多;一般來說,只可以用在 ++、—、+=、-=…這一類的運算元。

  • 而在這個例子中,除了用 atomic 犧牲效率來避免這個問題,也可以使用另一個位這種情形設計的 clause:reduction。

    reduction( 運算元 : 變數 )
    

原則上,它支援的運算元有 +, , –, &, ^, |, &&, ||;而變數則必須要是 shared 的;而他的運作方式,就是讓各個執行緒針對指定的變數擁有一份有起始值的複本(起始值是運算元而定,像 +, – 的話就是 0, 就是 1),然後在平行化的計算時,都以各自的複本做運算,等到最後再以指定的運算元,將各執行緒的複本整合。

int sum = 0;
#pragma omp parallel
{
    #pragma omp for reduction( +:sum)
    for( int i = 0; i < 10000; ++ i )
        for( int j = 0; j < 50000; ++ j )
            sum += x;
}
printf( "%dn",sum );

results matching ""

    No results matching ""