您的位置:首页 > 新闻 > 简单程序并行化-OpenMP

简单程序并行化-OpenMP

2023-10-01 13:11   

简单程序并行化方法-OpenMP(一)介绍

本文最初发表于:http://www.hongganji8.com/blog/cns!E0070FB8ECF9015F!1018.entry


嗯~ 首先,Heresy最近才开始尝试使用openMP,所以这篇文章与其说是教学或介绍,不如说是一次学习经历。您会继续使用它吗?说实话,现在还是个未知数。不管怎样,看看会发生什么吧~

我也希望如果有人对这个东西有研究的话可以给你一些建议。


多线程的概念

双核CPU目前备受关注。 AMD的Athlon64x2、Intel的Pentium-D、Core Duo以及即将推出的Core 2 Duo将成为下一代计算机的主流(尤其是超低成本的Pentium D,这绝对是一款具有极高性能的双核CPU)现阶段C/P值较高)。但双核有什么用呢?

对于一般的单线程程序,多核处理器无法提高其处理性能;但是,对于多线程程序,可以使用不同的核心同时计算,达到加速的目的!举个简单的例子,拿一个单线程程序来说,如果做一件事需要十秒,如果做十次而且都是同一个核心做的,自然就是10秒*10次,也就是100秒。 ;但对于多线程程序来说,它可以把这个任务分给两个核心,每个核心做5次,所以所需的时间只有50秒!

当然,多线程程序其实没那么简单。工作的切割和组合也需要更多的时间,所以现实中,即使在最好的条件下,双核的性能也不会是理想的1+1=2。此外,并不是所有的工作都是可切割的!很多任务都是相关的,如果直接划分到不同的处理核心并行运算,结果肯定是有问题的。而且,多线程程序的编写和维护比单线程程序复杂得多。

但是,如果计算机本身是多处理器、多核处理器,或者处理器具有像Intel超线程技术这样可以同时处理多个执行线程的功能,那么每一个都可以处理独立。如果把工作从单执行线程改为多执行线程,大部分执行效率都会得到提升!


多线程程序

写程序时如何写多线程程序?一般的方法就是真正利用线程控制,在程序中实际生成其他线程进行处理。 POSIX Threads 等库是用于生成和控制执行线程的函数库。 Microsoft Visual Studio 2005还提供了控制线程的功能。这种方式大多会产生多个线程,然后主线程将工作进行拆分,分配给各个线程进行计算。最后,主线程收集并整合结果。

但是,实际控制线程是相当麻烦的,程序的编写也会复杂很多。如果我们只想并行化一些简单的循环,我们可以使用线程库来控制它们。真是杀鸡儆猴的感觉。这时候使用Open MP就简单多了! OpenMP 是一个可以通过高级指令轻松实现并行化和多线程程序的 API。在最简单的情况下,您甚至可以只添加一行指令来并行化循环中的程序。知道了!


OpenMP 的基本使用

在Visual C++ 2005中使用openMP并不难,只需在Project的Properties中打开Language in C/C++的OpenMP Support(参数为/openmp),然后VC++2005就可以支持编译期间的 OpenMP。句法;使用OpenMP文件时,需要首先包含OpenMP头文件:omp.h。

如何并行化for循环?很简单,前面加一行就行了

#pragma omp 并行 for

够了!

您还可以实际使用一个简单的程序来弄清楚它是如何工作的。

#包括
#包括

void Test( int n )
{
for( int i = 0; i < 10000; ++ i )
{
//什么都不做,只是浪费时间
}
printf( "%d, ", n );
}

int main(int argc, char* argv[])
{
for( int i = 0; i < 10; ++ i )
测试( i );

系统(“暂停”);
}

上面的程序是main()中的一个非常简单的循环。它运行十次。每次调用 Test() 函数,并将循环 (i) 的执行次数传递给 Test()。并打印出来。当然,结果会是:

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

如果您想使用 OpenMP 并行化 main() 中的循环该怎么办?只需将其更改为以下内容即可:

#包括

#包括
#包括

void Test( int n )
{
for( int i = 0; i < 10000; ++ i )
{
//什么都不做,只是浪费时间
}
printf( "%d, ", n );
}

int main(int argc, char* argv[])
{
#pragma omp 并行 for
for( int i = 0; i < 10; ++ i )
测试( i );

系统(“暂停”);
}

很简单,对吧?从头到尾只加了两行(红色部分)!执行后可以发现结果发生了变化!

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

从结果可以明显看出,他并不是按照0到9的顺序跑的!上面的命令是怎么来的呢?其实很简单。 OpenMP 只是将循环 0 - 9 的十步拆分为 0 - 4、5 - 9 两部分,并将它们发送到不同的执行线程来运行,所以数字就会有这样的交错输出~

如何确认确实有多个线程在运行?如果有多个处理器、多核处理器或者超线程,单线程程序最多只会消耗一个核的使用量;例如,在Pentium 4 HT上运行单线程程序时,在作业管理器中看到的最大CPU使用率为50%。使用OpenMP并行化循环后,在执行循环时可以挤占两个核心的CPU!即作业管理员可以看到CPU使用率为100%。


当然,OpenMP并不是唯一有这个命令的~而且OpenMP并不是适合任何场合。请期待第二部分。 :p

但是理论上我们应该在测试完效率之后列出一些性能测试结果。同时,还会添加一些使用 OpenMP 的bugs 演示。


注:

  1. Microsoft VisualStudio 2005 Express 版本不附带 OpenMP 库。看来需要Standard或以上版本。
  2. OpenMP 官方网站:http://www.hongganji8.com
  3. Visual C++ 中的 OpenMP(Microsoft MSDN 库):http://www.hongganji8.com/en-us/library/tt15eb9t.aspx
  4. Visual C++ 编译器选项/openmp:http://www.hongganji8.com/zh-tw/library/fw509c3b.aspx

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

简单程序并行化-OpenMP(二)语法解释

本文最初发表于:http://www.hongganji8.com/blog/cns!E0070FB8ECF9015F!1280.entry


之前我已经简单介绍过多线程和OpenMP的并行化。有兴趣的可以参考《簡 易的程式平行化方法-OpenMP(一) 》。由于Heresy最近看了一些资料,做了一些测试,所以可能想谈谈我最近学到的一些语法~

首先,在Heresy的理解中,一般使用OpenMP的部分分为三类:

  1. 指令
  2. 子句
  3. 功能

功能部分独立调用。其实一般情况下好像很少用。指令和从句的用法大致应该是:

#pragma omp 指令 [] 条款 ]

形式为

。和之前的#pragma omp parallel for一样,实际上parallel和for都是指令;因此语法实际上可以分为 #pragma omp parallel#pragma omp for 两行。那就是

 #pragma omp 并行 for
for( int i = 0; i < 10; ++ i )
测试( i );

实际上是

 #pragma omp parallel

{
#pragma omp for
for( int i = 0; i < 10; ++ i )
测试( i );
}

组成。

OpenMP 指令列表如下:

原子内存地址将自动更新。该指令的目的是防止变量同时被修改而导致计算结果不正确。
指定将自动更新的内存位置。
屏障等待所有执行线程都到达屏障。用于同步。
同步团队中的所有线程;所有线程在屏障处暂停,直到所有线程执行屏障。
ritic 强制以下程序一次仅由一个线程执行。
指定代码一次仅在一个线程上执行。
flush 指定所有线程对所有共享对象具有相同的内存视图。
for 用于 for 循环 以前,循环是并行化的。 (注:回圈的索引只能是int)
导致并行区域内的for循环中完成的工作在线程之间划分。
master 指定由主执行绪来执行接下来的程序。
指定只有主线程应该执行程序的一部分。
ordered 指定程序将在并行 for 循环中顺序执行。
指定并行 for 循环下的代码应像顺序循环一样执行。
并行表示后面的程序将被并行化。
定义一个平行区域,是将由多个线程并行执行的代码。
部分 将使以下部分平行。
标识要在所有线程之间划分的代码段。
single 之后的程序只会在一个线程中执行,不会并行。
允许您指定一段代码应在单个线程(不一定是主线程)上执行。
threadprivate 指定一个变量是线程私有的。

虽然只有11个,但是已经有点大了;有些异端至今还不能确定它的用途和作用。

其中,要并行化,使用并行对于 这三项;指定使用单个执行线程,尤其是mastersinglecrigicalbarrier用于控制线程同步; ordered用于设置并行化的执行顺序。 atomicflushthreadprivate都应该用于控制变量。


至于子句部分,有以下13条:

复制

使threadprivate变量的值与主线程的值相同。
允许线程访问主线程的值(线程私有变量)。

复制私人

在不同线程中共享变量。
指定一个或多个变量应在所有线程之间共享。

default

设置并行化过程中变量处理的默认值。
指定并行区域中无作用域变量的行为。

第一个私人

让每个执行线程都有变量的副本,避免相互干扰;起始值将是开始并行化之前变量的值。
指定每个线程应该有自己的变量实例,并且应该使用变量的值来初始化该变量,因为它存在于并行构造之前。

if

可以通过判断条件来决定是否并行。
指定循环是并行执行还是串行执行。

最后私人

让每个执行线程都有变量的副本,避免相互干扰;当所有并行执行线程结束后,最终值将被写回主执行线程。
指定变量的封闭上下文版本设置为等于执行最终迭代(for 循环构造)或最后一个部分(#pragmasections)的线程的私有版本。

等一下

忽略障碍(等待)。
覆盖指令中隐含的屏障。

num_threads

设置并行期间的线程数。
设置线程组中的线程数。

ordered

用于 for。当并行化循环时,程序中标有ordered指令的部分可以顺序执行。
如果要在循环中使用有序(OpenMP 指令)指令,则在并行 for (OpenMP) 语句中需要。

private

让每个执行线程都有变量的副本,以避免相互干扰。
指定每个线程应该有自己的变量实例。

减少

对于各个执行线程的变量,将指定的操作数合并并写回主执行线程。
指定每个线程私有的一个或多个变量是并行区域末尾的归约操作的对象。

日程

设置for循环的并行化方式;有四种方法:动态、引导、运行时和静态。
适用于 for (OpenMP) 指令。

共享

设置该变量为所有线程共享(应该视为私有)。
指定一个或多个变量应在所有线程之间共享。

在子句中,copyincopyprivatedefault , 共享 , 私有 , firstprivate, lastprivate, reduction这8项都是用来控制并行化时变量的处理方式。 orderedschedule是控制并行时的执行顺序分配方式; num_threads , if 更像是一个控制执行线程的设备。决定。


在函数部分,MSDN 列出了二十多个函数;不过heresy觉得使用的机会并不高,所以这里就不列出来了。 (我懒得研究了:p)

虽然一般可能用不到,但测试时为了验证执行顺序以及多个线程之间的关系,有时候需要知道该线程是哪个线程在运行。这时候就可以显示使用omp_get_thread_num()函数来获取当前线程号。


注:

  1. 虽然VisualStudio 2005 Express也有OpenMP选项,但它实际上并没有附带OpenMP库,所以理论上不能使用;但是,如果您可以找到该文件的标准版或专业版并将其放入,它也可以工作。 !
  2. 令人惊奇的是...原则上,如果您不使用 OpenMP 函数,而仅使用指令和子句,则不需要 #include ;但在 Express 中,不用#include 也可以 也能正确编译并执行,但 Professional 版本只能正确编译而无法正确执行(dll 启动错误)。

参考:

  1. OpenMP 并行编程(1):http://www.hongganji8.com/dr Zhouweiming/archive/2006/08/28/1131537.aspx
    OpenMP 并行编程(2):http://blog.csdn .net/drzhouweiming/archive/2006/09/04/1175848.aspx
  2. MSDN 的 OpenMP:http://www.hongganji8.com/en-us/library/tt15eb9t.aspx

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

简单程序并行化 - OpenMP (3)并行示例,第

本文最初发表于:http://www.hongganji8.com/blog/cns!E0070FB8ECF9015F!1281.entry


在 OpenMP 中,并行化有三种方式: parallelsections 、 for(但section和for都需要平行)。这里,举一些例子来说明它们的操作。

用于测试的函数如下

void 测试( int n )
{
printf(" - %d
“,omp_get_thread_num() , n );
输出

输出形式为:Thread_id> -n


parallel

parallel 语法非常简单,就是#pragma omp parallel;但原则上,稍后您需要使用 {} 来指定范围。示例程序如下:

#包括
#包括
#包括

int main(int argc, char* argv[])
{
#pragma omp parallel
{
测试( 0 );
}
系统(“暂停”);
}

而这样的程序在双核计算机上的结果应该是:

 - 0
- 0

从结果可以看出,Test()被两个不同的线程执行一次,所以会输出两行;这是因为OpenMP会自动选择预设的线程数。

然后,对程序进行一些修改后,就变成了

#包括
#包括
#包括
#定义OMP 11

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

" );
系统(“暂停”);
}

而这样的程序在双核计算机上的结果应该是:

 - 0
- 0
- 0

程序中添加ifnum_threads 这两个语法。 num_threads 用于指定执行线程的数量。在上面的程序中,它被指定为 3,因此结果将是三个不同的线程,每个线程调用 Test()

if(OMP>10) 用于控制是否需要并行化;如果 #define OMP 11 更改为 #define OMP 9(或任何不大于 10 的数字),结果将为 Test() 只是调用一次并且只打印一行。不过,需要注意的一点是,使用 if 条件来停止并行化实际上应该将线程数设置为 1;也就是说,OpenMP 仍然会进行处理,但只会使用一次执行。线程运行。使用这种方法需要注意的是,如果曾经因为 if 而停止并行化,那么接下来的默认线程数也会变成1!所以如果确实想用 if来判断是否并行,最好在下一部分加上num_threads 来指定执行线程的数量。这是一个例子:

 #pragma omp 并行
测试( 1 );
#pragma omp 并行 if(false)
测试(2);
#pragma omp parallel
测试(3);

他执行的结果应该是:

 - 1
- 1
- 2
- 3

这是因为在执行Test(1)时,被并行化为两个线程,所以执行了两次。但到了 Test(2)时,因为执行了 if(false),所以关闭了并行化,设置为线程;同时,这也导致下面的Test(3)也变成只有一个线程。

在并行范围内,有一些指令可以使用;如singlemaster等。就像下面这个节目

#pragma omp 并行 num_threads(2)
{
for( int i = 0; i < 3; ++ i )
测试( i );
printf("嗨
" );
#pragma omp single
{
printf(“嗨,单身
" );
}
#pragma omp master
printf( "大师您好
" );
}

执行结果:

 - 0
- 0
- 1
- 1
- 2
- 2


嗨,单身
Hi,master

其中可以发现,带有singlemaster的部分程序只会被执行一次;和 mastersingle 两者的区别在于,master肯定会被主线程执行,而可能不会。

基本上,Heresy不确定什么时候会直接使用并行,所以没有添加更多的墨迹。 (什么时候需要多执行一个普通程序?@@)


sections

sections

sections #pragma 的用途omp节做块切割,然后OpenMP做并行处理。以下程序是一个简单的示例:

int main(int argc, char* argv[])
{
#pragma omp 平行部分
{
#pragma omp 部分
{
for( int k = 0; k < 100000; ++ k )
{}
测试( 0 );
}
#pragma omp 部分
{
测试( 1 );
}
#pragma omp 部分
{
测试(2);
}
#pragma omp 部分
{
测试(3);
}
}
}

执行结果为:

 - 1
- 2
- 3
- 0

从执行输出中我们可以发现:因为thread0先执行了执行时间最长的第一段,而在thread0结束第一段之前,其他三个段已经执行完毕线程 1 已结束 ~

但是使用sections进行并行化时,必须注意程序的依赖关系;如果两个程序是相关的,其实不适合用sections来并行化。化。以下是错误示例:

int a[5];
#pragma omp 平行部分
{
#pragma omp 部分
{
int k;
for( int i = 0; i < 5; ++ i )
{
a[i] = i;
for( k = 0; k < 10000; ++ k )
{}
}
}
#pragma omp 部分
{
for( int i = 0; i < 5; ++ i )
printf("%d
", a[i] );
}
}

其中,程序中for( k = 0; k < 10000; ++ k ){}的目的只是为了减慢时间。输出为:

0
1
-858993460
-858993460
-858993460

这是因为第一个部分部分执行速度较慢,所以当第二个部分时打印,也没有时间填写a[] 中的信息,因此会发生错误。

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

简单程序并行化 - OpenMP (4) 示例

本文最初发表于:http://www.hongganji8.com/blog/cns!E0070FB8ECF9015F!1283.entry


基本示例

对于循环的并行化是Heresy认为最实用的;因为只要循环中的内容是相互独立的,就可以通过并行化来加速。最简单的例子可能是:

#pragma omp 并行 for
for( int i = 0; i < 6; ++ i )
测试( i );

其中用于测试的函数Test如下

void 测试( int n )
{
for( int i = 0; i < 10000; ++ i )
for( int j = 0; j < 100000; ++ j )
int x = i ^ i ^ i ^ i;
printf(" - %d
“,omp_get_thread_num() , n );
}

中循环的目的只是为了浪费时间;输出格式将为: thread_id > - n 。执行该程序的结果将是

 - 0
- 3
- 1
- 4
- 2
- 5

可以看出,线程0将在循环中执行部分0、1、2,而线程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 );

來說,他的執行順序會是

 - 0
- 3
- 1
- 4
- 2
- 5

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

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

執行結果則變成

 - 0
- 1
- 2
- 3
- 4
- 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 );
}

結果會變成

 - 0
- 3
- 10
- 1
- 11
- 2
- 12
- 13
- 4
- 14
- 5
- 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://www.hongganji8.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( " - %d, %d
", omp_get_thread_num() , n, m );
}

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

 - 0, 0
- 2, 0
- 0, 1
- 2, 1
- 0, 2
- 2, 2
- 1, 0
- 1, 1
- 1, 2

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

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

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

 - 0, 0
- 2, 0
- 0, 1
- 2, 2
- 1, 0
- 2, 1
- 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 
#include
#include

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 )。

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

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

 

想象百科 |
© All Rights Reserved