簡易的程式平行化-OpenMP

簡易的程式平行化方法-OpenMP(一)簡介

本文原發表於:http://heresy.spaces.live.com/blog/cns!E0070FB8ECF9015F!1018.entry


嗯~首先,Heresy 也是最近才開始試著用 openMP 的,所以其是這篇與其說是教學或介紹,倒不如說是學習心得會更為恰當。會不會繼續用?說實話也是未知數。總之,看著辦囉~

也希望有人對這東西有 研究的話,能多多指教。


多執行緒的概念

目前雙核心的 CPU 當道,AMD 的 Athlon64x2、Intel 的 Pentium-D、Core Duo,以及即將上市的 Core 2 Duo,儼然將成為下一代電腦的主流(尤其是超低價的 Pentium D,絕對是現階段 C/P 值極高的雙核心 CPU)。但是雙核心有什麼用呢?

對於一般單一執行緒(single thread)的程式,多核心的處理器並沒有辦法提升它的處理效能;不過對於多執行緒(multi thread)的程式,就可以透過不同的核心同時計算,來達到加速的目的了!簡單的例子,以單執行緒的程式來說,一件事做一次要十秒的話,要做十次,都丟 給同一顆核心做的話,自然就是 10 秒 * 10 次,也就是 100 秒了;但是以多執行緒的程式來說,它可以把這一件事,分給兩顆核心各自做,每顆核心各做 5 次,所以所需要的時間就只需要 50 秒!

當 然,多執行緒的程式實際上沒這麼簡單。在工作的切割、結合上,也是要多花時間的,所以在現實中,即使最佳狀況,雙核心的效能也不會是 1 + 1 = 2 這樣的理想化。除此之外,也不是所有工作都是可以切割的!很多工作是有關聯性的,這樣如果直接切割給不同的處理核心各自去平行運算,出來的結果是肯定有問 題的。而且,多執行緒的程式在編寫、維護上,也都比單一執行緒的程式複雜上不少。

不過,如果電腦本身是多處理器、多核心處理器,或是 處理器擁有像 Intel Hyper-Threading Technology 這類的能在同一個時間處理多個執行緒的功能的話,那把各自獨立的工作由單一執行緒改成多執行緒,在執行的效率上,大多還是會有增進的!


多執行緒的程式

寫程式的時候該怎麼去寫多 執行緒的程式呢?一般的方法,就是真的利用 thread 的控制,去實際在程式中去產生其他的 thread 來處理。像 POSIX Threads 這套 library,就是用來產生、控制執行緒的函式庫。而像 Microsoft VisualStudio 2005 中,也有提供控制 thread 的功能。這種方法,大多就是產生多個 thread,而再由主要的 thread 把工作拆開,分給各 thread 去運算,最後再由主要的 thread 回收結果、整合。

但是,實際上要去控制 thread 是滿麻煩的~在程式的編寫上,也會複雜不少;而如果我們只是想要把一些簡單的迴圈平行化處理,用 thread library 來控制,實在有點殺雞用牛刀的感覺。這時候,用 Open MP 就簡單多了!OpenMP 是一種能透過高階指令,很簡單地將程式平行化、多執行緒化的 API;在最簡單的情形,甚至可以只加一行指令,就可以將迴圈內的程式平行化處理了!


OpenMP 的基本使用

要在 Visual C++ 2005 中使用 openMP 其實不難,只要將 Project 的 Properties 中 C/C++ 裡 Language 的 OpenMP Support 開啟(參數為 /openmp),就可以讓 VC++2005 在編譯時支援 OpenMP 的語法了;而在使用到 OpenMP 的檔案,則需要先 include OpenMP 的 header file : omp.h。

而要將 for 迴圈平行化處理,該怎麼做呢?非常簡單,只要在前面加上一行

#pragma omp parallel for

就夠了!

 

也可以實際用一段簡單的程式,來弄清楚 它的運作方式。

#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[])
{
for( int i = 0; i < 10; ++ i )
Test( i );

system( "pause" );
}

上面的程式,在 main() 是一個很簡單的迴圈,跑十次,每次都會呼叫 Test() 這個函氏,並把是迴圈的執行次數(i)傳進 Test()  並列印出來。想當然耳,它的結果會是:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

而如果想利用 OpenMP 把 mian() 裡面的迴圈平行化處理呢?只需要修改成下面的樣子:

#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 );

system( "pause" );
}

夠簡單吧?重頭到尾,只加了兩行(紅色部分)!而執行後,可以發現結果也變了!

0, 5, 1, 6, 2, 7, 3, 8, 4, 9,

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

而要怎麼確定真的 有跑多執行緒呢?如果本來有多處理器、多核心處理器或有 Hyper Thread 的話,一個單執行緒程式,最多隻會把一顆核心的使用量吃完;像比如說在 Pentium 4 HT 上跑,單一執行緒的程式,在工作管理員中看到的 CPU 使用率最多就是 50%。而利用 OpenMP 把迴圈進行平行化處理後,就可以在執行廻圈時,把兩顆核心的 CPU 都榨光了!也就是工作管理員可以看到 CPU 使用率是 100%。

 


 

當 然,OpenMP 不是隻有這個指令的~而且 OpenMP 也不適用於任何場合。請期待第二部份。 :p

不過理論上,應該會等測試過 效率後,來列一些效能測試的結果。同時,會加一些錯誤 利用 OpenMP 的示範。


註:

  1. Microsoft VisualStudio 2005 Express 版中,並沒有附上 OpenMP 這個 library,似乎是要 Standard 以上版本纔有。
  2. OpenMP 官方網站:http://www.openmp.org
  3. OpenMP in Visual C++ (Microsoft MSDN Library):http://msdn2.microsoft.com/en-us/library/tt15eb9t.aspx
  4. Visual C++ 編譯器選項 /openmp:http://msdn2.microsoft.com/zh-tw/library/fw509c3b.aspx

 

 

==============================================================================

簡易的程式平行化-OpenMP(二)語法說明

本文原發表於:http://heresy.spaces.live.com/blog/cns!E0070FB8ECF9015F!1280.entry


之前對於多執行緒和 OpenMP 的平行化已經做了些簡單的介紹,有興趣的可以回頭參考《簡 易的程式平行化方法-OpenMP(一) 》。而由於 Heresy 最近看了些資料,也做了些測試,所以主要可能想來講最近學的一些語法吧~

首 先,在 Heresy 的認知裡,一般會用到 OpenMP 的部分分為三類:

  1. Directives
  2. Clauses
  3. Functions

而 function 的部份是獨立呼叫的,其實在一般的情況下,似乎不大會用到。而 directive 和 clause 的用法,大致上應該是:

#pragma omp directive [clause ]

的形式。像之前 #pragma omp parallel for ,實際上 parallel 和 for 都是 directive;所以語法實際上可以拆開成 #pragma omp parallel  和 #pragma omp for 兩行。也就是

 #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 的 directive 列表如下:

 

atomic 記憶體位址將會自動更新。這個指令的目的在於避免變數備同時修改而造成計算結果錯誤。
Specifies that a memory location that will be updated atomically.
barrier 等待,直到所有的執行緒都執行到 barrier。用來同步化。
Synchronizes all threads in a team; all threads pause at the barrier, until all threads execute the barrier.
critical 強制接下來的程式一次只會被一個執行緒執行。
Specifies that code is only executed on one thread at a time.
flush Specifies that all threads have the same view of memory for all shared objects.
for 用在 for 迴圈之前,會將迴圈平行化處理。(註:迴圈的 index 只能是 int)
Causes the work done in a for loop inside a parallel region to be divided among threads.
master 指定由主執行緒來執行接下來的程式。
Specifies that only the master thread should execute a section of the program.
ordered 指定接下來被程式,在被平行化的 for 迴圈將依序的執行。
Specifies that code under a parallelized for loop should be executed like a sequential loop.
parallel 代表接下來的程式將被平行化。
Defines a parallel region, which is code that will be executed by multiple threads in parallel.
sections 將接下來的 section 平行化處理。
Identifies code sections to be divided among all threads.
single 之後的程式將只會在一個執行緒執行,不會被平行化。
Lets you specify that a section of code should be executed on a single thread, not necessarily the master thread.
threadprivate Specifies that a variable is private to a thread.

雖然只有 11 個,但是已經有點頭大了;有的 Heresy 還是不能確定它的用途、效果。

其中,要拿來平行化,是使用 parallelsectionsfor 這三項;而要指定使用單一執行緒,則是特過 mastersinglecrigical 這三項。barrier 則是拿來控制執行緒同步用的;ordered 是用來設定平行化的執行順序。atomicflushthreadprivate 則應該都是用來控制變數的。


而 clause 的部份,則有下列 13 個:

copyin

讓 threadprivate 的變數的值和主執行緒的值相同。
Allows threads to access the master thread's value, for a threadprivate variable.

copyprivate

將不同執行緒中的變數共用。
Specifies that one or more variables should be shared among all threads.

default

設定平行化時對變數處理方式的預設值。
Specifies the behavior of unscoped variables in a parallel region.

firstprivate

讓每個執行緒中,都有一份變數的複本,以免互相干擾;而起始值則會是開始平行化之前的變數值。
Specifies that each thread should have its own instance of a variable, and that the variable should be initialized with the value of the variable, because it exists before the parallel construct.

if

判斷條件,可以用來決定是否要平行化。
Specifies whether a loop should be executed in parallel or in serial.

lastprivate

讓每個執行緒中,都有一份變數的複本,以免互相干擾;而在所有平行化的執行緒都結束後,會把最後的值,寫回主執行 緒。
Specifies that the enclosing context's version of the variable is set equal to the private version of whichever thread executes the final iteration (for-loop construct) or last section (#pragma sections).

nowait

忽略 barrier(等待)。
Overrides the barrier implicit in a directive.

num_threads

設定平行化時執行緒的數量。
Sets the number of threads in a thread team.

ordered

使用於 for,可以在將迴圈平行化的時候,將程式中有標記 directive ordered 的部份依序執行。
Required on a parallel for (OpenMP) statement if an ordered (OpenMP Directives) directive is to be used in the loop.

private

讓每個執行緒中,都有一份變數的複本,以免互相干擾。
Specifies that each thread should have its own instance of a variable.

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 四種方法。
Applies to the for (OpenMP) directive.

 

shared

 

將變數設定為各執行緒共用(應該算是相對於 private 的)。
Specifies that one or more variables should be shared among all threads.

而在 clause 的中,copyincopyprivatedefaultsharedprivatefirstprivatelastprivatereduction 這 8 項,都是用來控制變數在平行化時的處理方法的。orderedschedule 是控制平行化時的執行順序分配方法;num_threadsif 則比較像是控制執行緒的設定。


而在 Function 的部份,MSDN 列了二十來個 function ;不過就 Heresy 感覺是用到機會似乎不高,所以在這邊就不列出來了。(也懶的研究了 :p)

雖 然一般可能用不到,但是在測試的時候,為了驗正執行的順序、多執行緒的關係,有時候必須要知道線在是由哪個執行緒在跑的~這時候,可以透過 omp_get_thread_num() 這個函式,來取得目前執行緒的編號。


註:

  1. 雖然 VisualStudio 2005 Express 也有 OpenMP 的選項,但是實際上並沒有附上 OpenMP 的函式庫,所以理論上是不能用的;不過如果能找到 Standard 或 Professional 版的檔案放進去,也是可以運作的!
  2. 神奇的是…原則上如果沒有用到 OpenMP 的 Function,而只是用 directive 和 clause 的話,應該是可以不用 #include <omp.h> 才對;但是在 Express 中,不加入 #include <omp.h> 可以正確的編譯、執行,而 Professional 版卻只能正確的編譯,而無法正確的執行(dll 起始錯誤)。

參考資料:

  1. OpenMP並行程序設 計(一):http://blog.csdn.net/drzhouweiming/archive/2006/08/28/1131537.aspx
    OpenMP 並行程序設計(二):http://blog.csdn.net/drzhouweiming/archive/2006/09/04/1175848.aspx
  2. MSDN 的 OpenMP:http://msdn2.microsoft.com/en-us/library/tt15eb9t.aspx

 

==============================================================================

簡易的程式平行化-OpenMP(三)範例 parallel、section

本 文原發表於:http://heresy.spaces.live.com/blog/cns!E0070FB8ECF9015F!1281.entry


OpenMP 裡,平行化的方式有三種:parallelsectionsfor (不 過 section 和 for 都需要 parrallel)。這裡,舉些例子來說明他們的運作。

而用來測試的函式 Test內容如下

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

 輸出的形式會是:<T:thread_id > - n


parrallel

parrallel 的語法很直接,就是 #pragma omp parallel ; 不過原則上,後面要用 {} 來指定 scope。範例程式如下:

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

int main(int argc, char* argv[])
{
#pragma omp parallel
{
Test( 0 );
}
system( "pause" );
}

而這樣的程式在一臺雙核心的電腦上,結果應該會是:

<t:0> - 0
<t:1> - 0

從結果可以看出來,Test() 被兩個不同的 thread 個別執行了一次,所以會輸出兩行;這是因為 OpenMP 會根據硬體,自動選擇預設的執行緒數目。

接著,在針對程式些修改,變成

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

int main(int argc, char* argv[])
{
#pragma omp parallel if(OMP>10) num_threads(3)
{
Test( 0 );
}
printf( "
==========================

" );
system( "pause" );
}

而這樣的程式在一臺雙核心的電腦上,結果應該會是:

<t:0> - 0
<t:2> - 0
<t:1> - 0

在程式中,加入了 ifnum_threads 這兩個語法。num_threads 是用來指定執行緒的數目的,而在上面的程式中,把它指定成 3,所以結果會由三個不同的 thread,個別呼叫一次 Test()

if(OMP>10) 則是拿來控制是否要平行化的條件;如果把 #define OMP 11 改成 #define OMP 9 (或者任何不大於 10 的數)的話,結果就會變成 Test() 只被呼叫一次,只印出一行。不過要注意的一點是,用 if 條件來停止平行化,實際上應該是將執行緒的數目設定成 1;也就是說 OpenMP 還是會做處理,但是會只用一個執行緒跑。而這樣使用要注意的就是,如 果曾經有因為 if 而停止平行化的話,接下來的預設的執行緒數目也會變成 1 !所以如果真的要用 if 來判斷是否要平行化,最好把接下來的部份,加上 num_threads 來指定執行緒的 數目。下面是一個例子:

#pragma omp parallel
Test( 1 );
#pragma omp parallel if(false)
Test( 2 );
#pragma omp parallel
Test( 3 );

他的執行結果應該會是:

<t:0> - 1
<t:1> - 1
<t:0> - 2
<t:0> - 3

這是因為在執行 Test(1) 的時候,有被平行化成兩個 thread,所以被執行了兩次。但是到了 Test(2) 的時候,因為執行了 if(false) ,所以將平行化關閉、設定為一個 thread;同時這也導致了接下來的 Test(3) 也變成只以一個 thread 來進行。

而在 parallel 的範圍內,還有一些 directive 是可以使用的;像是 singlemaster 等等。像下面的程式

#pragma omp parallel num_threads(2)
{
for( int i = 0; i < 3; ++ i )
Test( i );
printf( "Hi
" );
#pragma omp single
{
printf( "Hi, single
" );
}
#pragma omp master
printf( "Hi, master
" );
}

執行結果:

<t:0> - 0
<t:1> - 0
<t:0> - 1
<t:1> - 1
<t:0> - 2
<t:1> - 2
Hi
Hi
Hi, single
Hi, master

其中,可以發現加上 singlemaster 的部份的程式只會被執行一次;而 mastersingle 兩者間的差異,則是 master 會一定由主執行緒來執行,single  不一 定。

基本上,Heresy 不大確定什麼時候會直接用到 parrallel,所以也就不多加著墨了。(什麼時候會要把一般的程式多執行幾次啊? @@)


sections

sections 的用處,是把程式中沒有相關性的各個程式利用 #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

從執行的輸出結果可以發現:由於 thread0 先執行了執行時間最久的第一個 section,而在 thread0 結束第一個 section 前,其他三個 section 已經由 thread1 執行結束了~

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

int	a[5];
#pragma omp parallel sections
{
#pragma omp section
{
int k;
for( int i = 0; i < 5; ++ i )
{
a[i] = i;
for( k = 0; k < 10000; ++ k )
{}
}
}
#pragma omp section
{
for( int i = 0; i < 5; ++ i )
printf( "%d
", a[i] );
}
}

其中,程式裡 for( k = 0; k < 10000; ++ k ){} 的目的只是在拖慢時間。輸出結果是:

0
1
-858993460
-858993460
-858993460

這是因為第一個 section 的部份,執行的速度比較慢,所以當第二的 section 要列印的時候,還來不及將資料填入 a[] 裡,所以會導致錯誤。

==============================================================================

簡易的程式平行化-OpenMP(四)範例 for

本文原發表於:http://heresy.spaces.live.com/blog/cns!E0070FB8ECF9015F!1283.entry


基本例子

for 迴圈的平行化是 Heresy 認為最實用的一個;因為只要迴圈內的內容是互相獨立的,就可以透過平行化來加速。最簡單的例子,大概就是:

#pragma omp parallel for
for( int i = 0; i < 6; ++ i )
Test( i );

其中,而用來測試的函式 Test 內容如下

void Test( int n )
{
for( int i = 0; i < 10000; ++ i )
for( int j = 0; j < 100000; ++ j )
int x = i ^ i ^ i ^ i;
printf( "<T:%d> - %d
", omp_get_thread_num() , n );
}

 裡面的迴圈目的只是要浪費時間而已;輸出的形式會是:<T:thread_id > - n 。而這樣程式執行的結果則會是

<T:0> - 0
<T:1> - 3
<T:0> - 1
<T:1> - 4
<T:0> - 2
<T:1> - 5

可以看的出來,thread 0 會執行迴圈裡的 0, 1, 2,而 thread 1 則會執行 2, 4, 6 的部份。

在 效率上的變化呢?以 Heresy 測試的電腦來說,在平行化前,執行完的時間大約要 17000ms;而在平行化後,則只需要大約 8000ms 的時間(以上的時間是 debug 版測的,release 的話,VC2005 會把那些拖時間的迴圈忽略掉而無法進行比較)。不過其實不是所有的情形都這麼美好的,在微軟的 MSDN 文件 也 有提到

如果您在應用程式中只有一個迴圈,而它執行的時間少於 15 毫秒 (根據您電腦上的額外負荷調整),/openmp 也許就不適當,但如果超過這個時間,倒不妨考慮使用 /openmp

因 此,是否適合將迴圈平行化處理?其實是要看情形、甚至看電腦的。

 

不能平行化的例子

而前面也有提 到,要把迴圏平行化的話,要注意:「迴圈的執行內容必須要互相獨立」。否則會怎樣呢?這?堮?費 伯納西數列 來當例子(費伯納西數列的基本定義,就是每一項都是前兩項的合)。程式的寫法很簡單,如下:

int F[10];
F[0] = 0;
F[1] = 1;
for( i = 2; i < 10; ++ i )
F[i] = F[i-1] + F[i-2];
for( i = 0; i < 10; ++ i )
printf( "%d," , F[i] );

而執行輸出結果就是 0,1,1,2,3,5,8,13,21,34 。 不過如果把他很直接的透過 OpenMP 平行化的話,程式變成

int Fe[10];
Fe[0] = 0;
Fe[1] = 1;
#pragma omp parallel for num_threads(4)
for( i = 2; i < 10; ++ i )
Fe[i] = Fe[i-1] + Fe[i-2];
for( i = 0; i < 10; ++ i )
printf( "%d," , Fe[i] );

而跑出來的結果,就不見得正確了!像以 Heresy 跑出來的結果就是 0,1,1,2,3,5,-1717986920,1717986916,-4,1717986912 。 錯誤造成的原因是由於在計算 Fe[6] 的時候,計算 Fe[5]Fe[4] 的 thread 還沒有完成,因此會導致計算的結果不正確;而錯誤不一定會一樣,取決於各 thread 的計算速度。所以像這類的例子,就不適合用來平行化。

 

平行化的依序執行-ordered

ordered 的部份,則是 directive 和 clauses 同時使用。比如說以最簡單的例子

#pragma omp parallel for
for( int i = 0; i < 6; ++ i )
Test( i );

來說,他的執行順序會是

<T:0> - 0
<T:1> - 3
<T:0> - 1
<T:1> - 4
<T:0> - 2
<T:1> - 5

而如果加上 ordered 的話,程式變成

#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 交錯出現,而現在則變成 thread0 跑完後,再跑 thread1 了~而在實際運算的時間上,加了 ordered 後,又變到大約 17000ms,和沒有平行化之前相差不大。而 CPU 使用量的部份,在沒有平行化之前的用量大約是 50%(因為是雙核心系統),而平行化後使用量會變成是 100%;但是加上 ordered 後,卻變成 thread0 在進行的時候,使用量大約是 50%,而開始進行 thread1 的時候,使用量會變成 100%。所以,其實 Heresy 不大確定對 for 做平行化之後,加上 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() 不依照順序。

 

平行化程 式的執行順序-schedule

在將 for 迴圈平行化的時候,其實還有一個 schedule , 是可以拿來決定怎麼分割 for 迴圈的執行的。有四個選擇:dynamicguidedstaticruntime ; 其中,除了 runtime 外都可以指定 chunk_size。各種用法的詳細說明如下:

 

static OpenMP 會將 for 迴圈的所有 iteration 依序以指定 chunk_size 做切割成數個 chunk;然後再用 round-robin fashion 的方法(不知道這是啥?),將各個 chunk 指定給 thread 去執行。
如 果沒有指定 chunk_size 的話,OpenMP 則會根據 thread 的數目做最平均的分配。
When schedule(static, chunk_size) is specified, iterations are divided into chunks of a size specified by chunk_size. The chunks are statically assigned to threads in the team in a round-robin fashion in the order of the thread number. When no chunk_size is specified, the iteration space is divided into chunks that are approximately equal in size, with one chunk assigned to each thread.
dynamic 和 static 時一樣,OpenMP 會將 for 迴圈的所有 iteration 依序以指定 chunk_size 做切割成數個 chunk。但是 dynamic 時,chunk 的分配方法會是動態的;當 thread 執行完一個 chunk 後,他會在去找別的 chunk 來執行。
如 果沒有指定 chunk_size 的話,chunk_size 會被設定為 1。
When schedule(dynamic, chunk_size) is specified, the iterations are divided into a series of chunks, each containing chunk_size iterations. Each chunk is assigned to a thread that is waiting for an assignment. The thread executes the chunk of iterations and then waits for its next assignment, until no chunks remain to be assigned. Note that the last chunk to be assigned may have a smaller number of iterations. When no chunk_size is specified, it defaults to 1.
guided guided 的 chunk 切割方法和 static、dynamic 不一樣;他會以「遞減」的數目,來分割出 chunk。而 chunk 的分配方式,則是和 dynamic 一樣是動態的分配。而遞減的方式,大約會以指數的方式遞減到指定的 chunk_size。
如果沒有指定 chunk_size 的話,chunk_size 會被設定為 1。
When schedule(guided, chunk_size) is specified, the iterations are assigned to threads in chunks with decreasing sizes. When a thread finishes its assigned chunk of iterations, it is dynamically assigned another chunk, until none remain. For a chunk_size of 1, the size of each chunk is approximately the number of unassigned iterations divided by the number of threads. These sizes decrease approximately exponentially to 1. For a chunk_size with value k greater than 1, the sizes decrease approximately exponentially to k, except that the last chunk may have fewer than k iterations. When no chunk_size is specified, it defaults to 1.
runtime 原則上,這不是一個方法。設定成 runtime 的話,OpenMP 會在執行到的時候,再由環境變數 OMP_SCHEDULE 來決定要使用的方法。
When schedule(runtime) is specified, the decision regarding scheduling is deferred until runtime. The schedule kind and size of the chunks can be chosen at run time by setting the environment variable OMP_SCHEDULE. If this environment variable is not set, the resulting schedule is implementation-defined. When schedule(runtime) is specified, chunk_size must not be specified.

上表內容參考《MSDN: 2.4.1 for Construct  》,而如果要測試的話,可以參考《MSDN: schedule 》;不過他的 sample code 裡的

if ((i % SLEEP_EVERY_N) == SLEEP_EVERY_N)

似乎是錯誤的程式碼,建議改成

if ((i % SLEEP_EVERY_N) == 0)

==============================================================================

簡易的程式平行化-OpenMP(五) 變數的平行化

本文原發表於:http://heresy.spaces.live.com/blog/cns!E0070FB8ECF9015F!1286.entry  


在將程式平行化的時候,其實還可能碰到一些問題。其中一個最大、最有可能碰到的,就是平行化後,每個執行緒裡的變數的獨立與 否。下面是一個簡單的兩層迴圈的程式:

#pragma omp parallel for
for( int i = 0; i < 3; ++ i )
for( int j = 0; j < 3; ++ j )
Test2( i, j );

而裡面的函式 Test2 內容如下

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

 輸出的形式會是:<T:thread_id > - n , m 。這樣的寫法,執行結果是

<T:0> - 0, 0
<T:1> - 2, 0
<T:0> - 0, 1
<T:1> - 2, 1
<T:0> - 0, 2
<T:1> - 2, 2
<T:0> - 1, 0
<T:0> - 1, 1
<T:0> - 1, 2

這樣看起來是是沒問題的。但是如果把程式改成

int	i,	j;
#pragma omp parallel for
for( i = 0; i < 3; ++ i )
for( j = 0; j < 3; ++ j )
Test2( i, j );

的話,執行結果可能就會變成

<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

哪裡有問題呢?最直接的問題,3x3 迴圈應該要跑九次 Test() ,但是他只跑了 7 次。原因就是 OpenMP 會把在 parallel 的範圍以外宣告的變數,當成是所有執行緒共用的;所以在執行的時候,兩個執行續可能會同時修改到相同的 j , 導致迴圈執行的次數比預期的少。

而解決的方法,就是透過 OpenMP 的 private (詳見 MSDN ), 來讓每個執行緒對變數 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 ,因為預設值就已經是了~

而在 privateshare 的設定,OpenMP 還有提供一個 clause 叫做 default (詳 見 MSDN )。 他的功用,就是指定預設的範圍外變數配置方法;值可以是 sharednone 。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 外,還有兩個類似的用法,分別是 firstprivatelastprivate 。其中,firstprivate (詳見 MSDN )除 了有 private 的功能外,還會將各執行緒的 private 變數,設定為執行緒開始前的變數值(透過 copy constructor);而 lastprivate (詳見 MSDN )則 是會將變數在執行緒最後的值,寫回主執行緒(透過 assignment constructor)。下面是一個 firstpricate 的例子:

cA	A;
A.counter = 0;
#pragma omp parallel for
for( int i = 0; i < 4; ++ i )
A.Output();
A.Output();

其中,Class cA 的定義是:

class cA
{
public:
int counter;

void Output()
{
++counter;
printf( "%d (T:%d)
", counter, omp_get_thread_num() );
}
};

這樣的程式,A 這個變數會是 shared 的,所以結果會是

1 (T:0)
2 (T:1)
3 (T:0)
4 (T:1)
5 (T:0)

而如果將 #pragma omp parallel for 修改為 #pragma omp parallel for private(A) ,結果則變成:

-858993459 (T:0)
-858993459 (T:1)
-858993458 (T:0)
-858993458 (T:1)
1 (T:0)

這是因為 private(A) 會讓各執行緒擁有一份變數 A 的複本,但是卻不會替他指定起始值所造成的;而在 for 迴圈結束後,所有由 OpenMP 產生的複本將被自動的釋放,而改用回原來開始平行化前的變數 A (姑且稱他為正本好了~),所以輸出的值依然是 1(因為在平行化過程中,修改的都是複本裡的資料)。

而如果將 #pragma omp parallel for private(A) 修改為 #pragma omp parallel for firstprivate(A) ,結果則變成:

1 (T:0)
1 (T:1)
2 (T:0)
2 (T:1)
1 (T:0)

在平行化開始、變數 A 開始建立複本的時候,OpenMP 會自動賦予各複本和正本一樣起始值,也就是 A.counter 的值會是 0;而接著各執行緒會各自以自己的複本做運算,所以兩個執行緒都輸出 1, 2。而在迴圈結束後,和使用 private 一樣,所有 A 的複本會被釋放,重新開始使用沒被修改過的正本 A

而如果再將 firstprivate(A) 修改為 lastprivate(A) , 結果則變成:

-858993459 (T:0)
-858993459 (T:1)
-858993458 (T:0)
-858993458 (T:1)
-858993457 (T:0)

由於 lastprivate(A)private(A) 一樣,不會賦予變數 A 複本起始值,所以在迴圈中的數值會和使用 private(A) 時一樣不如預期;不過不同的是, lastprivate 會把最後的結果寫回正本,所以在迴圈結束後,正本 A 的值也變成最後結束的執行緒中複本 A 的值。

firstprivatelastprivate 似乎也可以同時使用,也就是寫成 #pragma omp parallel for firstprivate(A) lastprivate(A) ;其結果如下:

1 (T:0)
1 (T:1)
2 (T:0)
2 (T:1)
3 (T:0)

 

還有一點要注意的,就是在使用 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( "%d
",sum );

很直接的想法,結果應該會是 10,000 * 50,000 = 500,000,000 吧?但是 Heresy 實際跑的結果,輸出的值卻不是固定的,而是大約在 300,000,000 左右;這就是因為同時修改變數 sum 所造成的結果。而要避免這種情況,可以使用 atomic 這個 directive(詳見 MSDN ); 他的用處就是用來防止變數同時被多個執行緒修改。修改後的程式如下:

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( "%d
",sum );

而這樣執行的結果,就會是正確的值了~不過相對起來,為了避免同時修改的問題,執行上的速度也慢了不少;本來的程式只要 6000ms 左右就可以執行完成,而加入了 atomic 後,卻需要用到 17000ms 左右的時間。此外,atomic 也不是在所有情形都能用,限制也相當的多;一般來說,只可以用在 ++、--、+=、-=…這一類的運算元。

而在這個例子中,除了用 atomic 犧牲效率來避免這個問題,也可以使用另一個位這種情形設計的 clause:reduction (詳見 MSDN )。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( "%d
",sum );

而這樣的結果不但正確,執行速度也會大幅增加!像上面的例子就只需要 750ms 左右。

不過,atomicreduction 都有一個共同的限制,就是他們只能針對「scalar variable」來做。對於自己設計的 class type,就沒辦法了~此外,也不能用在 overload 過的 operator。

 

除了上述的 privatefirstpruvatelastprivate 外,還有專門給 global、namespace 或 static 變數用的 threadprivate (詳 見 MSDN ); 不過 Heresy 研究了好一陣子,還是不清楚他們到底有什麼用處。對於 threadprivate ,在 MSDN 裡也有比較簡易的說明;而和前面三種 private 比起來,threadprivate 在使用上則有比較多的限制,詳見 MSDN OpenMP Reference 2.7.1 threadprivate Directive

Heresy 自己在測試的時候,自訂 class 要使用 threadprivate 的話,該 class 似乎不能重新定義 constructor 或 destrcutor;但是 MSDN 的範例程式中的 struct 卻有有重定義 destructor,而該段範例程式在 Heresy 測試時是無法編譯的;或許是編譯器的選項要再做些調整吧? Heresy 本來是想透過定義 constructor 和 destructor 的方法來測試變數建立、結束的時間與次數,但是由於 threadprivate  不 支援這種 class,所以也沒辦法測試了。而由於 private 也可以用在 global 或 static 變數,也因此 Heresy 目前還是不大清楚他的確實用處是在哪裡。

不過由下面的程式,可以發現指定為 threadprivate  的 變數 X 似乎有著和被同時指定 firstprivatelastprivate 的變數 A 有一樣的效果。

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

int x = 1;
#pragma omp threadprivate( x )

int main() {
int a = 1;
#pragma omp parallel for firstprivate( a ) lastprivate(a)
for( int k = 0; k < 4; ++ k )
{
a += 1;
x += 1;
printf( "!a = %d, x = %d
", a, x );
}
printf( "
==========================
" );
printf( "!a = %d, x = %d
", a, x );
system( "pause" );
}

執行結果

!a = 2, x = 2
!a = 2, x = 2
!a = 3, x = 3
!a = 3, x = 3

==========================
!a = 3, x = 3

雖然不清楚到底怎麼用,不過還是提一下:使用 threadprivate   時,還有兩個 clause 可以搭配使用,就是 copyin copyprivate 。 其中,copyin 是「Allows threads to access the master thread's value, for a threadprivate variable.」,而 copyprivate 則是「Specifies that one or more variables should be shared among all threads.」(此 clause 只能用於 single )。

怎麼用…等之後研究出來再說 了。

==============================================================================

 

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