b2科目四模拟试题多少题驾考考爆了怎么补救
b2科目四模拟试题多少题 驾考考爆了怎么补救

OpenMP并行编程进行简单的探讨(一)(图)

电脑杂谈  发布时间:2021-06-03 13:05:03  来源:网络整理

实验平台:win7、VS2010

1.简介

并行计算机可以简单地分为共享内存和分布式内存。共享内存是指多个内核共享一个内存。现在的PC就是这种类型的(无论是只有一个多核CPU还是可以插多个CPU,他们有很多A核和一个内存),一般的大型计算机结合了分布式内存和共享内存结构,即,每个计算节点都是共享内存,节点是分布式内存。为了在这些并行计算机上获得更好的性能,并行编程是一个必要条件。目前流行的并行编程方法是在分布式内存结构上使用MPI,在共享内存结构上使用Pthreads或OpenMP。我们这里关注的是共享内存并行计算机,因为编辑本文的机器属于这种类型(普通桌面)。与 Pthreads 相比,OpenMP 更简单。对于我们这些关心算法,只需要最基本的线程关系控制(同步、互斥等)的人来说,OpenMP再合适不过了。

本文简要讨论了 Windows 上 Visual Studio 开发环境下的 OpenMP 并行编程。这篇文章参考了关于OpenMP条目的维基百科,(有OpenMP规范),关于OpenMP条目的MSDM和教科书“MPI和OpenMP并行编程(C语言版)”:

(v=vs.100).aspx 《MPI与OpenMP并行编程(C语言版)》第17章,Michael J. Quinn,陈文光译,清华大学出版社,2004

注意,OpenMP的最新版本是4.0.0,而VS2010只支持OpenMP2.0(2002版),所以这篇文章也是关于OpenMP2.0的,本文重点介绍使用OpenMP 获得的加速比接近核心数,所以 OpenMP2.0 也足够了。

2.第一个OpenMP程序

第一步:创建一个新的控制台程序

第2步:项目属性,在所有配置下,将“Configuration Properties>>C/C++>>Language>>OpenMP Support”修改为Yes(/openmp),如下图:

内存共享技术

第 3 步:添加以下代码:

 1 #include
 2 #include
 3 int main()
 4 {
 5     std::cout << "parallel begin:\n";
 6     #pragma omp parallel
 7     {
 8         std::cout << omp_get_thread_num();
 9     }
10     std::cout << "\n parallel end.\n";
11     std::cin.get();
12     return 0;
13 }

第四步:运行结果如下:

如你所见,我的电脑是 8 核的(严格来说是 8 线程的),这是我们实验室的一个小型工作站(最多支持 24 核)。

3.“第一个OpenMP程序”幕后,并行原理

OpenMP 由编译器指令、运行时库函数和一些与 OpenMP 相关的环境变量、数据类型和 _OPENMP 宏定义组成。 OpenMP之所以很简单,是因为只有50多页,而OpenMP2.0 Specification只有100多页。第 2 节“第一个 OpenMP 程序”的第 6 行“#pragma omp parallel”是编译器指令。 “#pragma omp parallel”下的语句会被多个线程并行执行(即执行多次),第8行的Omp_get_thread_num()是Run-time Library Function,omp_get_thread_num()返回代码所在的线程号当前正在执行中。

共享内存计算机上并行程序的基本思想是使用多线程将并行负载分配到多个物理计算核心,从而缩短执行时间(同时提高CPU利用率)。在共享内存的并行程序中,标准的并行模式是 fork/join 并行。这个基本模型如下图所示:

内存共享技术

其中,主线程执行算法的顺序部分。当遇到并行计算公式时,主线程派生(创建或唤醒)一些额外的线程。在并行区域,主线程和这些派生线程协同工作。当并行代码结束时,派生线程退出或挂起,控制流返回到单独的主线程,这称为收敛。对应第2节“第一个OpenMP程序”,第4行对应程序的开始,第4-5行对应串行部分,第6-9行对应第一个并行块(8个线程),第10行-13 对应 串行部分,13行对应程序结束。

简单来说,OpenMP程序就是在通用程序代码中加入Compiler Directives。这些编译器指令指示编译器如何处理后续代码(无论是多线程的还是同步的)。所以OpenMP需要编译器的支持。上一节的第2步是开启编译器的OpenMP支持。与 Pthreads 不同,OpenMP 下的程序员只需要设计高级并行结构。线程的创建和调度都是编译器自动生成的。

4. 编译器指令

4.1 通用格式

Compiler Directive 的基本格式如下:

#pragma omp 指令名称 [子句[ [,] 子句]...]

“[]”表示可选,每个Compiler Directive作用于以下语句(C++中“{}”括起来的部分是复合语句)。

Directive-name 可以是:parallel、for、sections、single、atomic、barrier、critical、flush、master、ordered、threadprivate(一共11个,只有前4个有可选子句)。

clause(子句)相当于对Directive的修改,定义一些Directive参数什么的。子句可以是:copyin(variable-list), copyprivate(variable-list), default(shared | none), firstprivate(variable-list), if(expression), lastprivate(variable-list), nowait, num_threads(num ) , 有序, 私有(变量列表), 减少(操作:变量列表), 调度(类型[,大小]), 共享(变量列表) (共13个).

例如“#pragma omp parallel”表示后面的语句会被多个线程并行执行,线程数由系统预设(一般等于逻辑处理器的数量,例如一个i5 4核8线程CPU有8个逻辑处理器),可以在指令中添加可选子句,如“#pragma omp parallel num_threads(4)”仍然表示后续语句会被多个线程并行执行,但是线程数是4。

4.2 详解

本节的叙述顺序与我的另一篇博文OpenMP编程汇总表相同,读者可以对照阅读,也可以快速预览OpenMP的所有语法。

如果没有特殊说明,所有程序都在Debug下编译运行。

平行

parallel 表示后续语句将由多个线程并行执行。这已经是众所周知的了。 “#pragma omp parallel”后面的语句(或语句块)称为并行区域。

您可以使用 if 子句进行条件并行化,并使用 num_threads 子句覆盖默认线程数:

1 int a = 0;
2 #pragma omp parallel if(a) num_threads(6)
3 {
4     std::cout << omp_get_thread_num();
5 }

int a = 7;
#pragma omp parallel if(a) num_threads(6)
{
    std::cout << omp_get_thread_num();
}

可见无法保证多线程的执行顺序。

private、firstprivate、shared、default、reduction、copyin 子句是为 threadprivate 指令保留的。

为了

第 2 节中的“第一个 OpenMP 程序”实际上并没有满足我们对并行程序的期望——我们一般不希望在多个线程上并行执行相同的代码,而是在一个计算密集型任务上,将其拆分,让多个线程分别执行各个部分的计算任务,从而达到缩短计算时间的目的。这里的关键是每个线程执行的计算是不同的(操作的数据不同或者计算任务本身不同),多个线程协同完成所有的计算。 OpenMP for 指令将C++ for 循环的多次迭代划分为多个线程(划分是指每个线程执行的迭代互不重复,所有线程的迭代加在一起就是C++ for 循环的所有迭代),这里 C++ for 循环在执行 C++ for 之前需要一些限制来确定循环次数。例如,C++ for 不应包含中断。 OpenMP for 作用于它之后的第一个 C++ for 循环。下面是一个例子:

内存共享技术_服务器集群 内存共享_服务器集群内存共享

1 const int size = 1000;
2 int data[size];
3 #pragma omp parallel
4 {
5     #pragma omp for
6     for(int i=0; ii)
7         data[i] = 123;
8 }

默认情况下,在上面的代码中,7和线程会从程序执行到“#pragma omp parallel”,再加上主线程中的8个线程(在我的机器上),1000次对于C++的迭代是分为8个连续的段——0-124次迭代由线程0计算,125-249次迭代由线程1计算,以此类推。你可能已经猜到了,具体的C++ for的每次迭代是如何程间分配的,可以通过子句schedule(type[,size])来指示,后面会详细介绍。

如果并行区域只包含一个for指令语句,上面的代码就是这种情况。此时,parallel 和for 可以“缩写”为parallel for。上面的代码等价于:

1 const int size = 1000;
2 int data[size];
3 #pragma omp parallel for
4 for(int i=0; ii)
5     data[i] = 123;

正确使用for指令有两个条件。首先是C++ for 满足一定的限制,否则编译器会报错。二是C++ for 每次迭代的执行顺序不影响结果的正确性。这是一个逻辑条件。示例如下:

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp for
4     for(int i=0; i<1000000; ++i)
5         if(i>999)
6             break;
7 }

编译器报错如下:

错误 C3010:“break”:不允许跳出 OpenMP 结构块

schedule(type[,size]) 设置 C++ for 的多次迭代如何在多个线程之间分配:

schedule(static, size) 针对每个连续的大小将所有迭代放入一个组中,然后将这些组轮流分配给每个线程。比如有4个线程,100次迭代, schedule(static, 5) will iterate: 0-4, 5-9, 10-14, 15-19, 20-24...assigned to 0, 1 , 2, 3, 0... 线程数。 schedule(static) 与 schedule(static, size_av) 相同,其中size_av等于迭代次数除以线程数,即迭代次数以线程数连续等分(或近似等分),schedule(dynamic, size)同样分组,然后每组依次分配给当前空闲的线程(因此称为dynamic)。 schedule(guided, size) 将迭代分组并分配给当前空闲的线程,初始组大小将迭代次数除以线程数,然后以指数方式逐渐减小到大小(除以2)轮到.schedule(runtime)的划分方法由环境变量OMP_SCHEDULE定义。

这里有几个例子,你可以先忽略关键指令:

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for
4     for(int i=0; i<9; ++i){
5         #pragma omp critical
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

上面的输出显示线程0执行迭代0-2,线程1执行3-5,编号2执行6-9,相当于schedule(static,3).

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for schedule(static, 1)
4     for(int i=0; i<9; ++i){
5         #pragma omp critical 
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

1 #pragma omp parallel num_threads(3)
2 {
3     #pragma omp for schedule(dynamic, 2)
4     for(int i=0; i<9; ++i){
5         #pragma omp critical 
6         std::cout << omp_get_thread_num() << i << " ";
7     }
8 }

Ordered 子句与ordered 指令一起使用,参见ordered 指令,nowait 为barrier 指令保留,private、firstprivate、lastprivate、reduce 为threadprivate 指令保留。

部分

如果for指令用于数据并行,则sections指令用于任务并行,表示后面的代码块包含多个线程并行执行的section块。下面是一个例子:

 1 #pragma omp parallel
 2 {
 3     #pragma omp sections
 4     {
 5         #pragma omp section
 6         std::cout << omp_get_thread_num();
 7         #pragma omp section
 8         std::cout << omp_get_thread_num();
 9     }
10 }

上面代码中的两个section块会被两个线程并行执行,多个section块的第一个“#pragma omp section”可以省略。这里有一些问题。有多少线程来执行这段代码? "#pragma omp parallel" 中没有子句,默认是 8 个线程(也在我的机器上),哪 2 个部分被使用?线程执行是不确定的。当section块超过8个时,一个线程会执行1个以上的section块。

同理,上面的代码可以“缩写”为平行段:

1 #pragma omp parallel sections
2 {
3     #pragma omp section
4     std::cout << omp_get_thread_num();
5     #pragma omp section
6     std::cout << omp_get_thread_num();
7 }

Nowait 子句为barrier 指令保留,private、firstprivate、lastprivate、reduction 子句为threadprivate 指令保留。

单人

表示该代码将仅由一个线程执行。具体线程不确定。示例如下:

1 #pragma omp parallel num_threads(4)
2 {
3     #pragma omp single
4     std::cout << omp_get_thread_num();
5     std::cout << "-";
6 }

这里,线程 0 执行了代码 4 和 5 的两行,其他三个线程执行了代码的第 5 行。

Nowait 子句是为barrier 指令保留的,private、firstprivate 和copyprivate 子句是为threadprivate 指令保留的。

大师

指令代码只会被主线程执行,功能与single指令类似,但使用single指令时具体线程不确定(可能是当时空闲的那个)。

关键

定义临界区,确保只有一个线程同时访问临界区。观察以下代码及其结果:

1 #pragma omp parallel num_threads(6)
2 {
3     std::cout << omp_get_thread_num() << omp_get_thread_num();
4 }

线程 5 在执行第三行代码时被线程 2 中断(不是每次运行时都可能被中断)。

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << omp_get_thread_num();
5 }

内存共享技术_服务器集群内存共享_服务器集群 内存共享

这一次,无论运行多少次,都不会连续出现两次,因为第四行代码在一个线程执行的同时,其他线程无法执行(这行代码很关键)部分)。

障碍

定义同步。所有线程执行到这一行后,所有线程都会继续执行以下代码,请看示例:

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << " ";
5     #pragma omp critical
6     std::cout << omp_get_thread_num()+10 << " ";
7 }

1 #pragma omp parallel num_threads(6)
2 {
3     #pragma omp critical
4     std::cout << omp_get_thread_num() << " ";
5     #pragma omp barrier
6     #pragma omp critical
7     std::cout << omp_get_thread_num()+10 << " ";
8 }

可以看出,这时候先打印一位数,再打印两位数,因为当所有线程执行到第5行代码时,必须等待所有线程执行到第5行代码第五行。此时,所有线程继续执行第7行及以后的代码,这就是所谓的同步。

说说for、sections、single指令、nowait子句的隐含屏障如下:

 1 #pragma omp parallel num_threads(6)
 2 {
 3     #pragma omp for
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6         std::cout << omp_get_thread_num() << " ";
 7     }
 8     // There is an implicit barrier here.
 9     #pragma omp critical
10     std::cout << omp_get_thread_num()+10 << " ";
11 }

 1 #pragma omp parallel num_threads(6)
 2 {
 3     #pragma omp for nowait
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6         std::cout << omp_get_thread_num() << " ";
 7     }
 8     // The implicit barrier here is disabled by nowait.
 9     #pragma omp critical
10     std::cout << omp_get_thread_num()+10 << " ";
11 }

节,单个指令是相似的。

原子

atomic 指令保证变量是原子更新的,即只有一个线程同时更新变量(是不是很像一个critical指令),看例子:

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         ++m;
6 }
7 std::cout << "value should be: " << 1000000*6 << std::endl;
8 std::cout << "value is: "<< m << std::endl;

m的实际值比预期的要小,因为“++m”的汇编代码有不止一条指令,假设三个:load、inc、mov(读RAM到寄存器,加1,写回内存)。有可能线程A执行到inc时,线程B执行load(线程A inc之后的值没有写回),然后线程A mov,线程B inc,然后mov,本来应该增加2到增加 1。

使用原子指令后可以得到正确的结果:

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         #pragma omp atomic
6         ++m;
7 }
8 std::cout << "value should be: " << 1000000*6 << std::endl;
9 std::cout << "value is: "<< m << std::endl;

是否可以使用关键指令?

1 int m=0;
2 #pragma omp parallel num_threads(6)
3 {
4     for(int i=0; i<1000000; ++i)
5         #pragma omp critical
6         ++m;
7 }
8 std::cout << "value should be: " << 1000000*6 << std::endl;
9 std::cout << "value is: "<< m << std::endl;

有什么区别?显然是效率。让我们做一个定量分析:

 1 #pragma omp parallel num_threads(6)
 2 {
 3     for(int i=0; i<1000000; ++i) ;
 4 }
 5 int m;
 6 double t, t2;
 7 m = 0;
 8 t = omp_get_wtime();
 9 #pragma omp parallel num_threads(6)
10 {
11     for(int i=0; i<1000000; ++i)
12         ++m;
13 }
14 t2 = omp_get_wtime();
15 std::cout << "value should be: " << 1000000*6 << std::endl;
16 std::cout << "value is: "<< m << std::endl;
17 std::cout << "time(S): " << t2-t << std::endl;
18 m = 0;
19 t = omp_get_wtime();
20 #pragma omp parallel num_threads(6)
21 {
22     for(int i=0; i<1000000; ++i)
23         #pragma omp critical
24         ++m;
25 }
26 t2 = omp_get_wtime();
27 std::cout << "value should be: " << 1000000*6 << std::endl;
28 std::cout << "value is: "<< m << std::endl;
29 std::cout << "time of critical(S): " << t2-t << std::endl;
30 m = 0;
31 t = omp_get_wtime();
32 #pragma omp parallel num_threads(6)
33 {
34     for(int i=0; i<1000000; ++i)
35         #pragma omp atomic
36         ++m;
37 }
38 t2 = omp_get_wtime();
39 std::cout << "value should be: " << 1000000*6 << std::endl;
40 std::cout << "value is: "<< m << std::endl;
41 std::cout << "time of atomic(S): " << t2-t << std::endl;

内存共享技术

按照惯例,需要列出机器配置:Intel Xeon Processor E5-2637 v2(4核8线程15M Cache,3.50 GHz),16GB RAM。以上代码需要在Release下编译运行才能获得更真实的运行时间(实际部署的程序不能是Debug版本)。第一个并行指令的目的是跳过潜在的线程创建,让下面三个并行指令具有相同的环境以增加可比性。从结果可以看出,当没有原子子句或临界子句时,运行时间要短得多。可以看出,正确性被性能取代了。不出所料,“过大而未充分利用”的关键子句的运行时间比原子子句的运行时间要长得多。

冲洗

表示所有线程对所有共享对象具有相同的内存视图。该指令表示变量的更新会直接写回内存(有时给变量赋值可能只会改变寄存器,然后再写回内存,这是编译器优化的结果)。这并不容易理解。看例子,为了让编译器对代码进行优化,需要在Release下编译运行如下代码:

 1 int data, flag=0;
 2 #pragma omp parallel sections num_threads(2) shared(data, flag)
 3 {
 4     #pragma omp section // thread 0
 5     {
 6         #pragma omp critical
 7         std::cout << "thread:" << omp_get_thread_num() << std::endl;
 8         for(int i=0; i<10000; ++i)
 9             ++data;
10         flag = 1;
11     }
12     #pragma omp section // thread 1
13     {
14         while(!flag) ;
15         #pragma omp critical
16         std::cout << "thread:" << omp_get_thread_num() << std::endl;
17         -- data;
18         std::cout << data << std::endl;
19     }
20 }

程序进入了死循环……我们的初衷是用flag来做手动同步。线程0修改data的值,修改后设置flag。线程 1 反复测试标志以检查线程 0 是否修改了数据。线程1 然后修改数据并打印结果。这里死循环的可能原因是线程 1 反复测试的标志只读取寄存器中的值,因为线程 1 认为只有它在访问标志(甚至认为只有一个线程),在它有修改了内存。无需再次将标志的值读入寄存器。使用flush指令修改后:

 1 int data=0, flag=0;
 2 #pragma omp parallel sections num_threads(2) shared(data, flag)
 3 {
 4     #pragma omp section // thread 0
 5     {
 6         #pragma omp critical
 7         std::cout << "thread:" << omp_get_thread_num() << std::endl;
 8         for(int i=0; i<10000; ++i)
 9             ++data;
10         #pragma omp flush(data)
11         flag = 1;
12         #pragma omp flush(flag)
13     }
14     #pragma omp section // thread 1
15     {
16         while(!flag){
17             #pragma omp flush(flag)
18         }
19         #pragma omp critical
20         std::cout << "thread:" << omp_get_thread_num() << std::endl;
21         #pragma omp flush(data)
22         -- data;
23         std::cout << data << std::endl;
24     }
25 }

这次的结果是正确的。解释一下,第 10 行代码告诉编译器确保数据的新值已经写回内存,而第 17 行代码说再次从内存中读取 flag 的值。

已订购

在带有有序子句的 for 指令(或并行 for)中使用,以确保代码将以迭代顺序执行(如串行程序),例如:

 1 #pragma omp parallel num_threads(8)
 2 {
 3     #pragma omp for ordered
 4     for(int i=0; i<10; ++i){
 5         #pragma omp critical
 6             std::cout << i << " ";
 7         #pragma omp ordered
 8         {
 9             #pragma omp critical
10                 std::cout << "-" << i << " ";
11         }
12     }
13 }

只看前面带“-”的数字是否有顺序,没有“-”的数字没有顺序。值得强调的是,for指令的ordered子句只与ordered指令配合使用,并不是迭代有序执行的意思。后者的代码是这样的:

服务器集群内存共享_服务器集群 内存共享_内存共享技术

1 #pragma omp for ordered
2 for(int i=0; i<10; ++i)
3     #pragma omp ordered{
4     ; // all the C++ for code
5 }

线程私有

将全局或静态变量声明为线程私有。要了解线程共享和私有变量,请查看以下代码:

1 int a;
2 std::cout << omp_get_thread_num() << ": " << &a << std::endl;
3 #pragma omp parallel num_threads(8)
4 {
5     int b;
6     #pragma omp critical
7     std::cout << omp_get_thread_num() << ": " << &a << "  " << &b << std::endl;
8 }

内存共享技术

记住第3-7行的代码需要8个线程执行8次。变量a程之间共享,变量b为每个线程一个(程自己的堆栈空间中)。

如何区分哪些变量是共享的,哪些是私有的?并行区域中定义的变量(非堆分配)当然是私有的。没有指定特殊子句(上面的代码是这样的),并行区之前定义的变量(并行区之后不可见,这和纯C++一样)是共享的,在堆中(由new或malloc 函数) 上面分配的变量是共享的(即使在多个线程中使用new或malloc,当然指向这个堆内存的指针可能是私有的),for指令使用的C++ for循环变量是私有的在哪里定义的。

好吧,回到 threadprivate 指令并查看示例:

 1 #include
 2 #include
 3 int a;
 4 #pragma omp threadprivate(a)
 5 int main()
 6 {
 7     std::cout << omp_get_thread_num() << ": " << &a << std::endl;
 8     #pragma omp parallel num_threads(8)
 9     {
10         int b;
11         #pragma omp critical
12         std::cout << omp_get_thread_num() << ": " << &a << "  " << &b << std::endl;
13     }
14     std::cin.get();
15     return 0;
16 }

内存共享技术

下面是最后几个没有提到的子句:private、firstprivate、lastprivate、shared、default、reduction、copyin、copyprivate子句,先看private子句:

1 int a = 0;
2 std::cout << omp_get_thread_num() << ": " << &a << std::endl;
3 #pragma omp parallel num_threads(8) private(a)
4 {
5     #pragma omp critical
6     std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
7 }

内存共享技术

内存共享技术

private 子句将变量 a 从默认线程共享变为线程私有,每个线程都会调用默认构造函数生成变量 a 的副本(当然这里没有 int 的构造函数)。

第一个private 子句和private 子句的区别在于它会用共享版本变量a 进行初始化。 lastprivate 子句将执行最后一次迭代 (for) 或最后一个节块 (sections) 的线程的私有副本复制到私有的共享变量中。 shared 子句与private 子句相反,将变量声明为shared。在下面的例子中,shared 子句可以省略:

 1 int a=10, b=11, c=12, d=13;
 2 std::cout << "abcd's values: " << a << " " << b << " " << c << " " << d << std::endl;
 3 #pragma omp parallel for num_threads(8) \
 4     firstprivate(a) lastprivate(b) firstprivate(c) lastprivate(c) shared(d)
 5 for(int i=0; i<8; ++i){
 6     #pragma omp critical
 7     std::cout << "thread " << omp_get_thread_num() << " acd's values: "
 8         << a << " " << c << " " << d << std::endl;
 9     a = b = c = d = omp_get_thread_num();
10 }
11 std::cout << "abcd's values: " << a << " " << b << " " << c << " " << d << std::endl;

内存共享技术

每个线程都修改了a、b、c、d的值。因为 d 是共享的,所以每个线程在打印 d 之前可能会被其他线程修改。在并行区域的末尾,a 的共享版本保持不变。因为 b 和 c 是由 lastprivate 子句声明的,所以执行最后一次迭代的线程会用自己的私有 b、c 更新 b、c 的共享版本和共享版本 d 的值。这取决于最后更新的线程 d.

default(shared|none):shared 参数与使用 share 子句定义所有变量相同。参数none表示未使用private、shared、reduction、firstprivate和lastprivate子句定义的变量报错。

reduction 子句用于归约。下面是一个并行求和的例子:

 1 int sum=0;
 2 std::cout << omp_get_thread_num() << ":" << &sum << std::endl << std::endl;
 3 #pragma omp parallel num_threads(8) reduction(+:sum)
 4 {
 5     #pragma omp critical
 6     std::cout << omp_get_thread_num() << ":" << &sum << std::endl;
 7     #pragma omp for
 8     for(int i=1; i<=10000; ++i){
 9         sum += i;
10     }
11 }
12 std::cout << "sum's valuse: " << sum << std::endl;

内存共享技术

可以看到变量 sum 对并行区域中的线程是私有的。每个线程用自己的sum对它的一部分求和,最后把所有线程的private sum加起来,分配给sum的共享版本。除了“+”归约,/、|、&&等都可以作为归约运算算法。

copyin 子句使 threadprivate 声明的变量的值与主线程的值相同,如下例所示:

 1 #include
 2 #include
 3 int a;
 4 #pragma omp threadprivate(a)
 5 int main()
 6 {
 7     a = 99;
 8     std::cout << omp_get_thread_num() << ": " << &a << std::endl << std::endl;
 9     #pragma omp parallel num_threads(8) copyin(a)
10     {
11         #pragma omp critical
12         std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
13     }
14     std::cin.get();
15     return 0;
16 }

内存共享技术

如果修改第9行代码去掉copyin子句,结果如下:

内存共享技术

copyprivate 子句允许所有线程共享不同线程中私有变量的值,例如:

1 int a = 0;
2 #pragma omp parallel num_threads(8) firstprivate(a)
3 {
4     #pragma omp single copyprivate(a)
5     a = omp_get_thread_num()+10;
6     #pragma omp critical
7     std::cout << omp_get_thread_num() << ": *" << &a << "  " << a << std::endl;
8 }

内存共享技术

可以写入copyprivate的变量必须是线程私有的。变量a满足这个条件。从上面的结果可以看出,single指令的代码是由第4个线程执行的,虽然第4个线程赋值了a,但只有这个线程是私有的,但是新的值会广播给其他线程的a,这就导致上述结果。

如果去掉copyprivate子句,结果变成:

内存共享技术

这次单指令代码由线程 0 执行。

嗯,我终于完成了。未完的事情请看另一篇文章:OpenMP Shared Memory Parallel Programming Summary Table。

6. 加速比

加速比是同一个程序的串行执行时间除以并行执行时间,即并行化的性能比串行化提高了数倍。理论上,加速比受以下因素影响:程序可并行化的比例、线程数、负载是否均衡(查看阿姆达尔定律)。另外,由于并行程序在实际执行过程中可能存在总线冲突,内存访问被称为瓶颈(以及Cache命中率问题),实际加速比一般低于理论加速。

为了查看加速比如何随着线程数的增加而变化,编写如下代码,需要在Release下编译运行:

 1 #include
 2 #include
 3 int main(int arc, char* arg[])
 4 {
 5     const int size = 1000, times = 10000;
 6     long long int data[size], dataValue=0;
 7     for(int j=1; j<=times; ++j)
 8         dataValue += j;
 9  
10     #pragma omp parallel num_threads(16)
11         for(int i=0; i<1000000; ++i) ;
12  
13     bool wrong; double t, tsigle;
14     for(int m=1; m<=16; ++m){
15         wrong = false;
16         t = omp_get_wtime();
17         for(int n=0; n<100; ++n){
18             #pragma omp parallel for num_threads(m)
19             for(int i=0; ii){
20                 data[i] = 0;
21                 for(int j=1; j<=times; ++j)
22                     data[i] += j;
23                 if(data[i] != dataValue)
24                     wrong = true;
25             }
26         }
27         t = omp_get_wtime()-t;
28         if(m==1) tsigle=t;
29         std::cout << "num_threads(" << m << ") rumtime: " << t << " s.\n";
30         std::cout << "wrong=" << wrong << "\tspeedup: " << tsigle/t << "\tefficiency: " << tsigle/t/m << "\n\n";
31     }
32  
33     std::cin.get();
34     return 0;
35 }

内存共享技术

可以看出,因为我们的程序运行在操作系统层面,而不是直接运行在硬件上,所以上面的测试结果看似不可思议——效率有时可以大于1!最好的加速出现在num_threads(8),大约是7.4,与8的物理核数非常接近,所以充分利用多核就这么简单。


本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-380138-1.html

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      热点图片
      拼命载入中...