2011年4月16日 星期六

OpenMP 心得 (五) Sections Construct

大部分的程式實際上是由許多個工作任務所組合而成,有些工作任務可能只是一行簡單的敘述,像是輸出資訊到螢幕;也可能是由數個敘述或小型任務所組成的程式區塊,例如條件判斷結構、迴圈結構、以及函式等。在些任務在序列執行的環境下按照我們編撰它們的順序被執行,其中有些任務在處理過程中是彼此相關的,即某些任務在運算時需要用到其它任務的執行結果,處理這類具有相關性的任務必須要按其因果順序來進行。但有些任務彼此間是不相關的,也就是說執行它們並不需要按一定的順序,誰先誰後都不會影響到程序接下來的進展以及最後的計算結果,像這樣互為獨立的程式任務就可以使用 OpenMP 的 sections construct 來將它們平行化。

一個 OpenMP sections construct 是由一個 sections directive (注意是複數的 sections) 再加上數個 section directive 所組成,其中每個 section directive 標誌著一個可獨立執行的結構化程式區塊。這些程式區塊必須滿足 OpenMP 對良好結構化程式的需求,也就是進出這些任務程式區塊的路徑只能有一條,其內容可由任意個可執行的指令敘述、各種程式結構、以及函式等程式元素來組成,只要區塊內的程式碼沒有包含跳逸出 sections construct 的指令即可,但能將整個程序結束的 C/C++ exit 函式或 Fortran 的 stop 敘述則允許出現在構造中。在 C/C++ 中建構 sections construct 的語法如下:
#pragma omp sections [clause[[,] clause] ...]
{
[#pragma omp section]
    structured-block
[#pragma omp section
    structured-block]
...
}
在 Fortran 中建構 sections construct 的語法為:
!$omp sections [clause[[,] clause] ...]
[!$omp section]
    structured-block
[!$omp section
    structured-block]
...
!$omp end sections [nowait]
第一個可被獨立執行的結構化程式區塊的前面可以不用加上 section directive,不過為了看起來比較一致,習慣上我還是會加上它以表示這個程式區塊是第一個 section directive 所作用的範圍。在 C/C++ 之中如果 section directive 之後緊接的可執行敘述超過一個以上,則必須明確地使用大括號 {} 將它們封裝成一個結構化區塊;而在 Fortran 中,一個 section directive 的作用範圍是以該 section directive 為起始點 (或以 sections directive 為起始點,如果 sections construct 內第一個獨立任務未使用 section directive 作標示的話) 到下一個 section directive 出現為止 (最後一個 section 的結束點為 sections construct 的結束宣告)。補充說明,section directive 是專用在 sections construct 內,不會應用在其它的 OpenMP constructs 之中。

因為 sections construct 在分類上是屬於 work-sharing constructs 的一員,所以在它結束的地方會有一個隱性的執行緒同步點存在,要移除這個同步點可以使用 nowait clause。使用 sections construct 還要記得一件事就是它並無新增執行緒的能力,所以整個 sections construct 必須嵌入在 parallel construct 之中才能夠平行處理程式。有關 sections construct 的運作概念可以使用下圖來表示:

負載平衡的考量
在 sections construct 內,每個結構化程式區塊都會分配到一個執行緒來處理它的工作,但我們無法事先得知程式區塊所分配到的執行緒是那一個,實際的分配情形是依運算當時作業系統調度執行緒的狀態來決定,過程中先遭遇到某個 section 任務的執行緒就負責處理該 section 的工作,其它的執行緒則繼續去找尚未被執行的 section。以這樣分配工作的方式會產生負載平衡 (load-balancing) 的問題:如果執行緒數目大於待執行的 section 數目,則多出來的執行緒會被系統閒置;反之若執行緒數目少於 section 的數目,則先完成工作的執行緒會接著去作尚未被處理的 section 任務,這樣就造成有些執行緒完成的 section 數量會比其它執行緒還要來得多。另外,每個 section 任務的運算量大小也不一定相同,有的需要花較長的時間去處理,而有的一下子就完成。先完成工作的執行緒如果在 sections construct 內找不到其它的工作來做就會待在 construct 的結束點等待其它的執行緒,除非在建構 sections construct 時有使用 nowait clause 來取消它的隱性同步點。

基本上我們將程式平行化的用意就是希望透過運算資源使用的最大化來改善程式的執行效能,因此若不能有效地平均分配每個執行緒的工作量就不能有效地利用系統資源。OpenMP 無法自動地依程式任務大小來規劃出最佳的工作量分配,這部分必須要靠使用者自己來作。舉例來說,我們可以在程式中先以一些計時函數測量出每個獨立任務所秏費的時間,然後再修改原程式中各個獨立任務的撰寫次序,盡量將幾個比較小型的獨立任務寫在一塊,以方便在 sections construct 中將它們分配給同一個 section。整體的改善目標是希望將每個 section 的運算量都調整到差不多一樣的大小,再使用適量的執行緒去執行程式以減少運算資源的浪費。

範例程式
底下我們示範 sections construct 的用法,照例先來看原始的序列程式:
//--Program: searial_sections.c
//--A Simple Program With Three Independent Tasks
//--Written by AAZ

#include <stdio.h>
#include <unistd.h>

void fun1(int);
void fun2(int);

int main()
{
    int k;

    fun1(3);

    fun2(5);

    for (k = 0; k < 5; k++) {
        printf("Hello\n");
    }
    return 0;
}

//----------------------------------------------------
void fun1(int n)
{
    int i, value;
    value = 0;
    for (i = 0; i <= n; i++) {
        value += i;
        sleep(1);
    }
    printf("fun1(%d) = %d\n", n, value);
}

//----------------------------------------------------
void fun2(int n)
{
    int i, value;
    value = 1;
    for (i = 1; i <= n; i++) {
        value *= i;
        sleep(1);
    }
    printf("fun2(%d) = %d\n", n, value);
}
在這個示範程式我們設計了三個獨立的任務,其中兩個任務以函式的型態來呈現,另一個任務則是使用 for-loop 在螢幕重複印出一些訊息。我們設計的函式對執行緒而言只有單一進出點,因此滿足 OpenMP 對結構化程式的需求。為了突顯執行緒分配任務的情形,我們使用了 unistd.h 的 sleep 函式拖延執行緒完成工作的時間,這樣在程式平行化後才不會因每個任務都執行過快導致系統只調用一個執行緒就完成 sections construct 內所有的任務。序列程式的執行結果如下:
由上圖可看出三個任務會按照我們撰寫程式碼的順序依次執行。接下來我們展示使用 sections construct 平行化的程式碼:
//--Program: omp_sections.c
//--Example of Using OpenMP Sections Construct 
//--Written by AAZ

#include <stdio.h>
#include <unistd.h>
#include <omp.h>

void fun1(int);
void fun2(int);

int main()
{
    int k, tid, threads;
    #pragma omp parallel private(tid)
    {
    #pragma omp sections
    {
    #pragma omp section
    fun1(3);

    #pragma omp section
    fun2(5);

    #pragma omp section
    {
    tid = omp_get_thread_num();
    threads = omp_get_num_threads();
    printf("Thread %d: Total Threads = %d\n", tid, threads);
    for (k = 0; k < 5; k++) {
        printf("Thread %d: Hello\n", tid);
    } //--End of for-loop
    } //--End of structured block of last section 
    } //--End of Sections Construct
    } //--End of Parallel Construct
    return 0;
}

//---------------------------------------------------------
void fun1(int n)
{
    int i, tid, value;
    tid = omp_get_thread_num();
    value = 0;
    for (i = 0; i <= n; i++) {
        value += i;
        sleep(1);
    }
    printf("Thread %d: fun1(%d) = %d\n", tid, n, value);
}

//---------------------------------------------------------
void fun2(int n)
{
    int i, tid, value;
    tid = omp_get_thread_num();
    value = 1;
    for (i = 1; i <= n; i++) {
        value *= i;
        sleep(1);
    }
    printf("Thread %d: fun2(%d) = %d\n", tid, n, value);
}
在 sections construct 中,每個任務以 section directive 作為執行緒分配工作的界限 (注意,第一個任務是可以不用加上 section directive)。我們在第三個任務增加一些取得運算環境資訊的敘述,但多了這些指令後我們就必須以大括號 {} 將 section 內所有的程式都封裝起來,讓它們成為一個有效的結構化程式區塊。另外,兩個獨立函式 fun1 與 fun2 也分別加入印出執行緒編號的指令,以方便我們觀察執行緒在平行運算過程中的分配情形。為了突顯之前提到過的負載平衡問題,以下我們示範使用 4 個執行緒來執行這個程式的結果:
由執行結果可以印證兩件事:
  • 1. 執行緒分配工作是任意的,不會按照工作的撰碼順序與執行緒編號來分配,每個 section 內的工作是由第一個遭遇到它的執行緒來負責運算。例如在我們的執行結果中第一個任務 fun1 是由編號最後的執行緒 (TID = 3) 所處理。
  • 2. 如果執行緒數目比 sections construct 內待分配的獨立任務還多時,則系統只調用足夠數量的執行緒去執行程式。例如在範例執行結果中可以看到 TID = 2 的執行緒並沒有參與平行運算的過程。
接下來展示範例程式的 Fortran 版本:
c--Program: omp_sections.f
c--Example of Using OpenMP Sections Construct 
c--Written by AAZ

      program main
      use omp_lib
      integer k, tid, threads

  100 format (A, I1, A, I1)
  200 format (A, I1, A)

!$OMP PARALLEL PRIVATE(tid)

!$OMP SECTIONS
!$OMP SECTION
      call fun1(3)

!$OMP SECTION
      call fun2(5)

!$OMP SECTION
      tid = omp_get_thread_num()
      threads = omp_get_num_threads()
      write(*,100)"Thread ", tid, ": Total Threads = ", threads 
      do k = 1, 5, 1
          write(*,200) "Thread ", tid, ": Hello"
      end do 
!$OMP END SECTIONS

!$OMP END PARALLEL 

      stop
      end program main

c---------------------------------------------------------------
      subroutine fun1(n)
      use omp_lib
      integer value, i, n, tid
  100 format (A, I1, A, I1, A, I1)

      value = 0
      do i = 0, n, 1
          value = value + i
          call sleep(1)
      end do

      tid = omp_get_thread_num()
      write(*,100)"Thread ", tid, ": fun1(", n, ") = ", value

      return 
      end subroutine fun1

c---------------------------------------------------------------
      subroutine fun2(n)
      use omp_lib
      integer value, i, n, tid
  100 format (A, I1, A, I1, A, I3)

      value = 1
      do i = 1, n, 1
          value = value * i
          call sleep(1)
      end do

      tid = omp_get_thread_num()
      write(*,100)"Thread ", tid, ": fun2(", n, ") = ", value

      return  
      end subroutine fun2
程式的架構與 C 語言範例大致相同,我們就不再多做說明。唯一要注意的是我們將 fun1 與 fun2 寫成 subroutine 的型式,所以記得要在每個 subroutine 內都匯入 omp_lib 才能正確地在副程式中叫用 OpenMP 函式。程式的執行結果如下:

parallel sections Construct
Sections construct 與 loop construct 一樣可與 parallel construct 合併成一個複合結構,叫作 parallel sections construct。它在 C/C++ 的建構語法如下:
#pragma omp parallel sections [clause[[,] clause] ...]
{
[#pragma omp section]
    structured-block
[#pragma omp section
    structured-block]
...
}
在 Fortran 的語法為:
!$omp parallel sections [clause[[,] clause] ...]
[!$omp section]
    structured-block
[!$omp section
    structured-block]
...
!$omp end parallel sections [nowait]
底下為本文 C 語言範例程式改用 parallel sections construct 的程式碼與執行結果,Fortran 版本就不再列出。
//--Program: omp_parallel_sections.c
//--Example of Using OpenMP Parallel Sections Construct 
//--Written by AAZ

#include <stdio.h>
#include <unistd.h>
#include <omp.h>

void fun1(int);
void fun2(int);

int main()
{
    int k, tid, threads;
    #pragma omp parallel sections private(tid)
    {
    #pragma omp section
    fun1(3);

    #pragma omp section
    fun2(5);

    #pragma omp section
    {
    tid = omp_get_thread_num();
    threads = omp_get_num_threads();
    printf("Thread %d: Total Threads = %d\n", tid, threads);
    for (k = 0; k < 5; k++) {
        printf("Thread %d: Hello\n", tid);
    } //--End of for-loop
    } //--End of structured block of last section 
    } //--End of Parallel-Sections Construct
    return 0;
}

//---------------------------------------------------------
void fun1(int n)
{
    int i, tid, value;
    tid = omp_get_thread_num();
    value = 0;
    for (i = 0; i <= n; i++) {
        value += i;
        sleep(1);
    }
    printf("Thread %d: fun1(%d) = %d\n", tid, n, value);
}

//---------------------------------------------------------
void fun2(int n)
{
    int i, tid, value;
    tid = omp_get_thread_num();
    value = 1;
    for (i = 1; i <= n; i++) {
        value *= i;
        sleep(1);
    }
    printf("Thread %d: fun2(%d) = %d\n", tid, n, value);
}

執行結果:

本章總結
  • 程式中獨立的任務可使用 sections construct 來平行化。
  • Sections directive 並不能新增執行緒,因此 sections construct 必須放在 parallel construct 之中。
  • Section directive 只能出現在 sections construct 之中,作為執行緒分配任務的界限。
  • 使用 sections construct 平行程式前應先設法將每個 section 內的程式執行時間調整到差不多,並使用適量的執行緒數目來執行程式以免浪費系統資源。

(發佈日期:2011/04/16 by AAZ)

沒有留言:

張貼留言