现在的位置: 首页 > 综合 > 正文

英特尔® 线程处理工具和 OpenMP

2012年10月25日 ⁄ 综合 ⁄ 共 4141字 ⁄ 字号 评论关闭

英特尔® 线程处理工具和 OpenMP
显式线程方法(如,Windows* 线程或 POSIX* 线程)使用库调用创建、管理并同步线程。使用显式线程,需要对几乎所有受影响的代码进行重新构建。OpenMP* 是编译指示(pragma)、API 函数,及环境变量的集合,能够以相对较高的级别将线程放入应用中。penMP 编译指示用于指出代码中能够并行运行的域。兼容 OpenMP 的编译器可转换该代码,并插入适当的函数调用以并行执行这些域。多数情况下,可以保留源代码的串行逻辑,编译时只需忽略 OpenMP 编译指示即可轻松恢复。

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

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

 

 #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 秒的时间内,就可以得出一个适当的实施保护方法。然而,假定一段大得多的代码,其并行区域拥有成百上千行代码,或者代码涉及大量不同的函数调用,在这些调用中参数通过指针或不同的变量名进行引用。 现在,找出潜在的存储冲突则不那么容易了。幸运的是,英特尔® 线程检查器可自动识别需要某种形式独占访问的变量。对上文实例代码添加编译指示后,通过英特尔® 线程检查器运行该代码,将发现在缺少某种并行形式时,变量 limit、prime、j、number_of_primes、number_of_43primes 以及 number_of_41primes 都会导致存储冲突。通过查看源代码以及对每个变量的尝试使用,我们能够判断如何最佳地对原始源代码进行修改,从而实施所需的变量作用域。

任何在读取前写入并行区域中的变量,以及变量值不需要在并行区域外使用的变量,都应设为私有(private)。对于 PrimeFinder* 实例代码,limit、prime 和 j 即为这种变量,它们仅在并行区域中作为 workspace(工作间)或临时变量使用。因此,我们能够通过使用 OpenMP 编程指示的私有语句为每个线程分配拷贝。其余三个计数器变量需要在并行区域后放置打印的全局总数,在这种情况下,我们应将它们设为共享变量,但需要在关键代码段内执行这些计数器的增量。所产生的并行域代码如下:

 

#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 的结构化特性,英特尔® 线程分析器能够为应用假定执行模块,并指出非常明确的性能问题。两个常见问题即负载不均衡与同步开销。我们应了解英特尔® 线程档案器如何识别这些问题,并对一些可行的解决方案进行讨论。

抱歉!评论已关闭.