Intel OpenMP

来源:互联网 发布:成品油批发价年度数据 编辑:程序博客网 时间:2024/05/16 05:03

  作者 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
  #include
  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(私有)或采用独占访问进行保护。当完成对原始代码的更改后,通过英特尔 多线程分析器运行代码所获得的结果支持对并行执行的调试,以充分利用系统中的计算资源。
  英特尔 多线程工具常常被认为能够迅速找出代码中的线程错误,并指出不明显的性能错误。通过在开发阶段较早地采用这些工具,您能够使系统自动运行这些枯燥的任务,从而在串行应用中找出于何处能够有效实施并行。
  
  

本文转自
http://blog.csdn.net/yangdelong/archive/2007/07/19/1698568.aspx

原创粉丝点击