介绍
在串行应用中找出于何处能够有效实施并行。

作者 Clay P. Breshears

显式线程化方法(如,Windows* 线程或 POSIX* 线程)使用库调用创建、管理并同步线程。使用显式线程,需要对几乎所有受影响的代码进行重新构建。此外,OpenMP* 是 pragma(编译指示)、API 函数,及环境变量的集合,能够支持以相对较高的级别将线程放入应用中。OpenMP 编译指示用于指出在代码中能够同时运行的区域。支持 OpenMP 的编译器对代码进行转换,并插入适当的函数调用,来并行执行这些区域。在大多情况下,可以保留原始代码的串行逻辑,并在编译时通过忽略 OpenMP 编译指示实现轻松恢复。

OpenMP 程序为线程化程序,同显式线程化应用一样,OpenMP 程序要面对相同的错误和性能问题。因为本文主要检查英特尔® 多线程工具、英特尔® 多线程检查器,以及英特尔® 多线程分析器的使用来分析 OpenMP 程序,所以我们假设读者熟悉 OpenMP。对于英特尔® 多线程检查器,并不采用在线程化代码中识别存储冲突的标准方法;相反,我们使用诊断输出,在并行区域内识别并对变量的作用域进行分类。而后,本文讨论了在 OpenMP 代码中会常常遇到的两类性能问题。举例说明如何使用英特尔® 多线程分析器来识别这些问题,并提供了一些可行的解决方案。欲了解英特尔® 多线程工具的详细信息,请参阅线程工具文档《英特尔® 多线程检查器入门》与《英特尔® 多线程分析器入门》。

为了更具体地阐述重点,选择对实施 brute force(力迫)算法的代码进行分析,该代码用于找出用户定义整数范围内的素数。串行代码挑出每个可能的素数(不考虑偶数),并除以所有小于或等于其平方根的整数。如果有某个测试因数可将其整除,则该数为合数;如果没有因数能将其整除,则为素数。找出的素数可随意输出,但通常需要计算所找出素数的总数。我们知道大于 2 的素数能够分为 2 类:其形式分别为 4n+1 与 4n-1。除了计算找到素数的总数以外,素数相关类(被 4 除后的余数)的计算也随之增加。采用的串行代码,PrimeFinder,如下所示。

#include <stdio.h>
#include <math.h>
main(int argc, char *argv[])
{
int i, j, limit;
int start, end;          /* range of numbers to search */
int number_of_primes=0;  /* number of primes found */
int number_of_41primes=0;/* number of 4n+1 primes found */
int number_of_43primes=0;/* number of 4n-1 primes found */
int prime;               /* is the number prime? */
int print_primes=0;      /* should each prime be printed? */
start = atoi(argv[1]);
end = atoi(argv[2]);
if (!(start % 2)) start++;
if (argc == 4 && atoi(argv[3]) != 0) print_primes = 1;
printf("Range to check for Primes: %d - %d/n/n",start, end);
for(i = start; i <= end; i += 2) {
limit = (int) sqrt((float)i) + 1;
prime = 1; /* assume number is prime */
j = 3;
while (prime && (j <= limit)) {
if (i%j == 0) prime = 0;
j += 2;
}
if (prime) {
if (print_primes) printf("%5d is prime/n",i);
number_of_primes++;
if (i%4 == 1) number_of_41primes++;
if (i%4 == 3) number_of_43primes++;
}
}
printf("/nProgram Done./n %d primes found/n",number_of_primes);
printf("/nNumber of 4n+1 primes found: %d/n",number_of_41primes);
printf("/nNumber of 4n-1 primes found: %d/n",number_of_43primes);
}
 
  
作为 OpenMP* 编程助手的英特尔® 多线程检查器
在给出的代码中,仅有一处逻辑位置能够插入 OpenMP 编程指示:主计算 for 循环。将 for 循环起始处代码更改为:
在缺省状态下,共享所有变量(不包括循环叠代变量)。通常,一些线程需要某些变量的专用拷贝,从而避免数据竞跑。另外,如果对这些变量的访问是同步的,则能够更好地实现程序的逻辑。在决定如何对共享变量访问进行最佳保护之前,我们必须识别哪些变量需要这种保护。

在这种简短的实例中,我们能够预计,即使仅有少量 OpenMP 使用经验的程序员,也只需不超过 30 秒的时间来识别需要保护的变量;在下一个 30 秒的时间内,就可以得出一个适当的实施保护方法。然而,请考虑一段大得多的代码,其并行区域拥有成百上千行代码,或者代码涉及大量不同的函数调用,在这些调用中参数通过指针或不同的变量名进行引用。现在,找出潜在的存储冲突则不那么容易了。

幸运的是,英特尔® 多线程检查器可自动识别需要某种形式独占访问的变量。 对上文实例代码添加 pragma 后,通过英特尔® 多线程检查器运行该代码,将发现在缺少某种并行形式时,变量 limit、prime、j、number_of_primes、number_of_43primes, 以及 number_of_41primes ,都会造成存储冲突。 请参阅下面英特尔® 多线程检查器的屏幕抓图。

多线程检查器的屏幕抓图。

通过查看源代码以及对每个变量的尝试使用,我们能够判断如何最佳地对原始源代码进行修改,从而实施所需的变量作用域。

任何在读取前写入并行区域中的变量,以及变量值不需要在并行区域外使用的变量,都应设为私有(private)。对于 PrimeFinder* 实例代码,limit、prime 以及 j 即为这种变量,它们仅在并行区域中作为 workspace(工作间)或临时变量使用。从而,我们能够通过使用 OpenMP 编程指示的私有语句为每个线程分配拷贝。 剩余的 3 个 counter 变量用于在并行区域执行完毕后放置输出的总数。因此我们将它们设为共享变量,但在关键代码段中执行这些 counter 的增量。接下来的并行区域代码为:

#pragma omp parallel for private (limit, j, prime)
for(i = start; i <= end; i += 2) {
limit = (int) sqrt((float)i) + 1;
prime = 1; /* assume number is prime */
j = 3;
while (prime && (j <= limit)) {
if (i%j == 0) prime = 0;
j += 2;
}
if (prime) {
if (print_primes) printf("%5d is prime/n",i);
#pragma critical
{
number_of_primes++;
if (i%4 == 1) number_of_41primes++;
if (i%4 == 3) number_of_43primes++;
}
}
}
 
通过英特尔® 多线程检查器运行该代码显示无额外的错误诊断。我们已创建了正确的线程化代码。作为 private 语句的替代,可以将受影响的变量本地放入 for 循环,而后进入并行区域。如果这些变量并不在代码的其它地方使用,则这种解决方案更为完善。 这种替代实施方案的另一个优势即,对于变量而言串行代码与并行代码更加匹配。

除了找出需要保护的变量外,英特尔® 多线程检查器还能判断某个代码段是否参与了并行。此外,对于长代码段或具有深层调用堆栈的代码而言,判断在潜在并行循环中是否具有任何的依赖性(dependency)是非常枯燥而耗时的工作。若不具备某种算法更改消除依赖性,则诸如递归变量(循环的每次叠代都会增加该变量)或递推关系(在前一个循环叠代上计算访问信息)等依赖性会阻碍正确的并行。英特尔® 多线程检查器指出存储冲突,程序员对代码进行检查,从而确认变量的使用构成了循环依赖。
利用英特尔® 多线程分析器进行性能调试
当创建了正确的线程化代码后,应该对该代码的性能进行测定。 可以轻松比较串行与线程化代码的执行时间。 当在双核系统上采用两个线程运行代码时,如果线程化代码执行时间是串行代码的一半,则说明已完美地实施了并行。 如果线程化代码的执行时间与串行代码的执行时间接近(甚至超过),则一定是出现了某种问题。 大段代码是否仍串行执行? 所需的同步是否对执行性能产生了负面影响? 每个线程的工作数量是否完全平衡?

针对 OpenMP 的英特尔® 多线程分析器用于回答这些问题,并指引程序员在代码中找出可以进行改进的代码,从而实现更好的并行性能。 鉴于 OpenMP 的结构化特性,英特尔® 多线程分析器能够为应用对执行模块进行假设,并非常明确地指出性能问题。 两种常见问题即负载不均衡与同步开销。 应了解英特尔® 多线程分析器如何识别这些问题,并对一些可行的解决方案进行讨论。

负载不均衡

在并行计算期间,空闲处理器即为浪费的资源。 与之类似,空闲线程也为浪费的资源,并对并行执行的整体运行时间产生负面影响。 缺省状态下,在每个 OpenMP 并行区域或任务分割(worksharing)区域结束时,线程以隐式的限制进行等待,直到所有的线程都完成了区域中分配的工作。 当分配给线程的计算不均衡时,计算任务较少的线程会在区域限制中处于空闲状态,直到计算任务较多的线程完成其工作。

在含超线程(HT)技术的双核处理器上,采用 4 个线程通过英特尔® 多线程分析器运行经过更改的 PrimeFinder 代码,将发现有相当多的一部分时间(14%)花费在空闲线程上。 由于在该实例代码中仅有一个单独的并行区域,所以可以很明显地找出产生不均衡的位置。 然而,当在更复杂的代码中查找不均衡的来源时,则需使用 Regions View(域浏览)来查找没有分配给线程足够工作的区域。 点击并行区域中的横条,即可查看相应区域的源代码。
为了更好地判断产生不均衡的原因,可以在选择的问题区域中咨询 Threads View(线程浏览)。 实例代码的 Threads View 如下。

通常来说,不均衡时间呈阶梯方式不断较低,这说明了尽管工作负载根据每线程的任务数量进行平均分配,但它仅仅增加了所需计算的数量, 在这种情况下,查找素数所需要查看的整数数量由叠代的缺省静态分布平均划分。 然而,需要查看的因数数量随着整数大小的增加而增加。 与前三个线程相比,检查最后四分之一整数范围的线程执行的计算更多。
这种不均衡方式同样说明了,任务的大小和序列是固定的。 可以通过建立更为动态的任务分配,来修改观察到的不均衡,或需检查的整数。 针对编译指示向并行区域添加 schedule 语句,能够使您更好地控制叠代分配至线程的方式。 采用具有足够块大小的动态 schedule,如 schedule(dynamic,100),能够根据需要分配叠代,以使计算更具分布性,同时还可为每个块提供足够的工作,来保持较低的规划开销。 针对实例代码不均衡的,一个不太明显的 schedule 为 schedule(static,1)。 该 schedule 可以象分纸牌一样,向线程分配叠代: 一个线程一个叠代,以轮转方式通过线程中循环,直到所有的叠代都分配完毕。 下面是实例代码的英特尔® 线程档案器 Summary View(摘要显示),该代码对需要测试的整数采用动态调度。

如果不均衡非常平均地分布于所有线程,特别是平均分布于那些在执行期间耗费成倍时间的区域,则负载很可能从一条路径改变至下一条路径。即,有一些较大型的任务将分配至线程,但是却没有方法来预计如何分配任务。为了对这种情况进行补救,可尝试将较小的任务动态分配至线程。


同步影响
尽管在新的测试代码中等待时间已经很小,但是线程还需花费一部分时间用于等待同步。利用负载不均衡,能够从实例代码清楚地了解到,在何处线程争用同步。对于更加复杂的情况,可使用英特尔® 多线程分析器的 Regions View 来判断哪些并行域包含这种冲突,并使您将精力主要集中于这些地方。

由同步保护的代码段应尽可能地简短,并保持正确的代码。采用这条原则,可以将代码花费在等待访问受保护代码段的时间降至最低。对于实例代码而言,没有任何外来声明(extraneous statement)不需要线程独占访问。每个声明都应放置在独立的关键代码段中。在这种情况下,我们应该对关键代码段进行命名,因为在 OpenMP 中,所有未命名的关键代码段都视作同一代码段,即便它们以完全不同的函数与源文件出现也是如此。 为了在实例代码中使用命名的关键代码段,将 counter 的增量改为:

#pragma omp critical (cs1)
number_of_primes++;
#pragma omp critical (cs2)
if (i%4 == 1) number_of_41primes++;
#pragma omp critical (cs3)
if (i%4 == 3) number_of_43primes++;

采用 4 个线程与 3 个关键代码段,则很有可能至少有一个线程将等待进入关键代码段。此外,进入与退出多个关键代码段的开销将变为 3 倍。英特尔® 多线程分析器得出的数据表明,当采用 3 个命名关键代码段覆盖原始的 3 线(three-line)关键代码段时,线程等待锁定的时间百分比与并行开销扩大了 2 倍。

另一条需要遵循的经验为,不要在循环中放置同步。针对实例代码,此处有 3 种选择来遵循该建议:局部变量;原子操作;OpenMP 归约语句。

用于每个 counter 的临时变量(这些临时变量声明用于在每个线程中创建专有拷贝),在循环中进行增加。退出循环前,当任务分割构成后,在一个单独的关键代码段中将局部变量值添加入全局变量值中。从而,仅在代码中的一处线程可能会延迟其它线程。 该方法的一个缺陷即,需要对串行代码进行更改来满足并行要求。

#pragma omp parallel 

{ int numPrimes=0;    /* local number of primes found */

int num41Primes=0;  /* local number of 4n+1 primes found */

int num43Primes=0;  /* local number of 4n-1 primes found */

#pragma omp for schedule(dynamic,100)

for(i = start; i <= end; i += 2) {

int limit, j, prime;

limit = (int) sqrt((float)i) + 1;

prime = 1; /* assume number is prime */

j = 3;

while (prime && (j <= limit)) {

if (i%j == 0) prime = 0;

j += 2;

}

if (prime) {

if (print_primes) printf("%5d is prime/n",i);

numPrimes++;

if (i%4 == 1) num41Primes++;

if (i%4 == 3) num43Primes++;

}

} // end for

#pragma omp critical

{ // Update global counter values with local values

number_of_primes += numPrimes;

number_of_41primes += num41Primes;

number_of_43primes += num43Primes;

}

} // end parallel region

 

 

第二种方法利用原子操作来避免存储冲突,并降低同步的影响。一种常用方法即采用 OpenMP 原子结构(atomic construct)。3 个 counter 的增量即可同原子操作共同使用的可接受形式之一。 实际上,早先的解决方案同样能够采用原子结构来保护全局更新(利用来自每个线程的私有、部分和)。在 Windows 环境中,固有的 InterlockedIncrement 可用于执行这 3 个共享 counter 的原子增量。


#pragma omp parallel 

{ int numPrimes=0;    /* local number of primes found */

int num41Primes=0;  /* local number of 4n+1 primes found */

int num43Primes=0;  /* local number of 4n-1 primes found */

#pragma omp for schedule(dynamic,100)

for(i = start; i <= end; i += 2) {

int limit, j, prime;

limit = (int) sqrt((float)i) + 1;

prime = 1; /* assume number is prime */

j = 3;

while (prime && (j <= limit)) {

if (i%j == 0) prime = 0;

j += 2;

}

if (prime) {

if (print_primes) printf("%5d is prime/n",i);

#pragma omp atomic

number_of_primes++;

if (i%4 == 1) {

#pragma omp atomic

number_of_41primes++;

}

#pragma omp atomic

if (i%4 == 3) {

#pragma omp atomic

number_of_43primes++;

}

}

} // end for

} // end parallel region

 

第三种方法同样采用 OpenMP 功能,来执行与第一种方法相同的操作,不同的地方是该方法不对串行代码进行任何更改。OpenMP 归约语句创建共享变量的专用拷贝,利用每个线程中的这些专用拷贝进行计算,并将所有的部分结果合并返回至并行区域末端的原始变量。归约语句的语法要求在并行区域完成时,将关联二元运算符与变量名称同操作符相结合。在对实例代码完成此项更改后(详情参见下列代码),英特尔® 多线程分析器所获得的结果可实现几近完美(99.984%)的并行执行。


 

       

总结
当遵循以上全部建议后,实例代码的最终并行区域为:
为获得该最终代码,首先采用英特尔® 多线程检查器判断串行代码中是否具有可并行执行的循环,以及需要将哪些变量设为 private(私有)或采用独占访问进行保护。当完成对原始代码的更改后,通过英特尔® 多线程分析器运行代码所获得的结果支持对并行执行的调试,以充分利用系统中的计算资源。

英特尔® 多线程工具常常被认为能够迅速找出代码中的线程错误,并指出不明显的性能错误。通过在开发阶段较早地采用这些工具,您能够使系统自动运行这些枯燥的任务,从而在串行应用中找出于何处能够有效实施并行。


intel openmp相关推荐

  1. Intel Movidius - Install the Intel Distribution of OpenVINO Toolkit for Raspbian* OS (树莓派系统) 环境构建

    Intel Movidius - Install the Intel Distribution of OpenVINO Toolkit for Raspbian* OS (树莓派系统) 环境构建 ht ...

  2. MPI学习存在的一些问题

    最近修改MPI程序,遇到了一些细节问题,在此标记一下,不知是MPI自身缺陷还是我不是很精通MPI, 有些问题还是不太理解,敬请各位专家批评指正 1.MPI_Reduce各进程的数据操作问题 比如说,对 ...

  3. SSH远程连接:简单的连接

    在已知用户名和ip的情况下,简答的ssh如下: rongtao@rongtao:~$ ssh Toa@172.18.174.84 Toa@172.18.174.84's password: Last ...

  4. 并行计算——OpenMP加速矩阵相乘

    OpenMP是一套基于共享内存方式的多线程并发编程库.第一次接触它大概在半年前,也就是研究cuda编程的那段时间.OpenMP产生的线程运行于CPU上,这和cuda不同.由于GPU的cuda核心非常多 ...

  5. OpenMP知识点汇总

    1. OpenMP(Open Multi-Processing)官网:http://openmp.org/wp/ 2. OpenMP最新版本4.0,2013年7月发布.Visual Studio 20 ...

  6. (原創) 如何在VC8使用OpenMP? (C/C++) (VC++) (OpenMP)

    Abstract 隨著多核心CPU普及,但時脈卻越來越低,若程式還是用單核心的方式去寫,不僅壓榨不出多核心的效能,執行速度還可能越來越低,所以使用平行處理(Parallel Programming)也 ...

  7. 用openMP进行并行加速

    用openMP进行并行加速 参考:http://blog.csdn.net/lanbing510/article/details/17108451 最近在看多核编程.简单来说,由于现在电脑CPU一般都 ...

  8. OpenMP的一点使用经验

    参考:http://blog.sina.com.cn/s/blog_7462bf390101d8sd.html 最近在看多核编程.简单来说,由于现在电脑CPU一般都有两个核,4核与8核的CPU也逐渐走 ...

  9. OpenMP在ARM-Linux以及NDK中的编译和使用

    OpenMP在ARM-Linux以及NDK中的编译和使用 参考:http://blog.sina.com.cn/s/blog_602f87700102w1ki.html 以前对OpenCV在ARM-L ...

最新文章

  1. 吴恩达deeplearning.ai五项课程完整笔记了解一下?
  2. UISegmentedControl (待续)
  3. git 一口气带你走完git之旅
  4. Core Data系列三——基本使用
  5. clickhouse mysql引擎_ClickHouse为啥能做到变态快,超 MySQL 801倍?
  6. Microsoft Expression Studio 3中文版
  7. validation 开始日期 结束日期_Spring Boot集成validation用于优雅的校验API参数的合法性...
  8. 方维P2P添加富友金账户接口实例
  9. 目标检测综述学习笔记
  10. 改了计算机名字后重启断网了,电脑断网重启就好了是什么回事
  11. android+代码调用+相册+小米,Android调用系统相册选择图片,支持小米4云相册
  12. 网站URL网址末尾是否应该使用反斜杠
  13. c语言学生成绩设计思路,C语言学生成绩管理系统设计.docx
  14. 认识substrate
  15. java发送html模板的高逼格邮件
  16. Linux命令之中英文对照
  17. Ubuntu解决风扇高速导致声音大的问题
  18. OpenCV找圆系列(2)HoughCircles算子新增了HOUGH_GRADIENT_ALT方法,效果好多了
  19. 《平凡的世界》《白鹿原》《废都》读后感
  20. Golang实现端口检测

热门文章

  1. python界面编程-PyQT5界面编程简单原理
  2. 面向对象分析与设计示例:骰子游戏
  3. Android Sqlite 简单SQL语句
  4. Pyhton 正则替换字符串指定内容
  5. 【ae】蒙版描边、音频波形显示
  6. 老男孩python全栈s21day15作业
  7. python-conda更新python(python升级)
  8. 梯度下降与随机梯度下降概念及推导过程
  9. PR模板 时尚创意板式设计镂空文字开场视频PR模板
  10. Storwize V7000剖析:走进存储系统的SVC