| 2008年04月03日 09:36 | |
简介
作为白皮书三部曲中的最后一篇,本文将为您介绍经验丰富的 C/C++ 程序员如何开始使用 OpenMP*,以及如何在应用中简化线程的创建、同步以及删除工作。我们的一系列白皮书将为您全面揭秘 OpenMP,其中第一篇向您简要介绍了 OpenMP 最常见的特性:循环工作共享。第二篇告诉您如何充分利用非循环并行能力及如何使用同步指令。最后一篇则讨论了库函数、环境变量、如何在发生错误时调试应用,以及最大限度地发挥性能的一些技巧。
运行时库函数
您可能还记得,OpenMP 由一套编译指令、函数调用和环境变量组成。前两篇文章只讨论了编译指令,本文将重点探讨函数调用和环境变量。这样安排的理由很简单:编译指令是 OpenMP 的“原因”,它们提供最大程度的简易性,不需要改变源代码,并且您可忽略它们来生成代码的系列版本。另一方面,使用函数调用需要改变程序,这将为执行系列版本(如需)带来困难。如果遇到疑问,请您在计划使用函数调用(包括 标头文件)时,尝试使用编译指令并保留函数调用。当然,您还应继续使用英特尔® C++ 编译器命令行切换 /Qopenmp。进行链接不需要其它库。
下表列出了四个最常使用的库函数,分别用于检索线程总数,设置线程数,返回当前线程数,及返回可用逻辑处理器的数量。如欲获得 OpenMP 库函数的全部列表,请务必访问 OpenMP 网站:www.openmp.org*
。

以下为使用上述函数来打印字母表的例子。
下表列出了四个最常使用的库函数,分别用于检索线程总数,设置线程数,返回当前线程数,及返回可用逻辑处理器的数量。如欲获得 OpenMP 库函数的全部列表,请务必访问 OpenMP 网站:www.openmp.org*
。| int omp_get_num_threads(无效); | 返回当前使用的线程数。如果在并行区域外被调用,则此函数的返回值为 1。 |
| int omp_set_num_threads(int NumThreads) | 此函数用于设置进入并行区域将使用的线程数。它可撤销 OMP_NUM_THREADS 环境变量。 |
| int omp_get_thread_num(无效); | 返回数值在 0(主线程)和线程总数 -1 之间的当前线程数。 |
| int omp_get_num_procs(无效); | 返回可用处理器数量。含超线程技术的处理器将被算作两颗处理器。 |
以下为使用上述函数来打印字母表的例子。
omp_set_num_threads(4); |
该例子阐述了使用函数调用(而非编译指令)的几个重要概念。首先,您必须重新编写代码。重新编写就意味着额外的记录、调试、测试和维护工作。其次,没有 OpenMP 的帮助很难甚或不可能进行编译。再次,极易产生缺陷;例如在上述循环中,如果线程数不是 26 的倍数,则循环就无法打印出字母表上的所有字母。如果您没有额外费心去创建自己的工作队列算法,那么最终您将会失去调整循环调度的能力。您将受制于自己的调度——极可能像上述例子中的静态调度。
环境变量
OpenMP 规范规定了两个常用环境变量(如下表所示)。
通常可以使用其它特定编译器环境变量。请务必检查您的编译器文件,了解其它变量信息。
| 环境变量 | 说明 | 实例 |
| OMP_SCHEDULE | 控制 for 循环工作共享结构的调度。 | 设置 OMP_SCHEDULE= "guided, 2" |
| OMP_NUM_THREADS | 设置默认线程数。使用 omp_set_num_threads() 函数调用可撤销该值。 | 设置 OMP_NUM_THREADS =4 |
调试
调试线程化应用十分棘手,因为调试器会改变运行时性能,掩盖竞态条件。甚至打印声明都可以掩盖问题,因为它们使用同步和操作系统函数。而 OpenMP 进一步加剧了情况的复杂性。OpenMP 会插入私有变量、共享变量和其它代码,该代码如果不借助专用的 OpenMP 感知调试器就无法查出和越过。因此,关键的调试手段是消除流程。
首先,请您务必意识到大多数错误都是竞态条件。大多数竞态条件都是由实际应被声明为私有变量的共享变量引起的。首先应查看并行区域内的变量,确保这些变量在需要时被声明为私有。同时,应检查并行结构内部被调用的函数。在默认情况下,堆栈上声明的变量为私有变量,但 C/C++ 关键词 static 会改变将放在全局堆上的变量,从而使其被 OpenMP 循环共享。下表所示的 default(none) 子句可用于协助找到那些难于发现的变量。如果您指定了 default(none),则每个变量都必须采用数据共享属性子句予以声明。
首先,请您务必意识到大多数错误都是竞态条件。大多数竞态条件都是由实际应被声明为私有变量的共享变量引起的。首先应查看并行区域内的变量,确保这些变量在需要时被声明为私有。同时,应检查并行结构内部被调用的函数。在默认情况下,堆栈上声明的变量为私有变量,但 C/C++ 关键词 static 会改变将放在全局堆上的变量,从而使其被 OpenMP 循环共享。下表所示的 default(none) 子句可用于协助找到那些难于发现的变量。如果您指定了 default(none),则每个变量都必须采用数据共享属性子句予以声明。
#pragma omp parallel for default(none) private(x,y) |
另一个常见错误是使用未经初始化的变量。请谨记私有变量在进入并行结构时没有初始值。请仅在需要时使用 firstprivate 和 lastprivate 子句来对它们进行初始化,否则会增加大量额外的开销。
如果您仍然找不到缺陷的所在,原因可能是您使用了过多的代码。请尝试寻找二进制。通过在并行结构上使用 if(0) 或完全注释掉编译指令来强制并行部分再次连续。另一个方法是强制大部分并行区域成为临界区。挑选一个您认为包含缺陷的代码区域,将其置于临界区内。试着找出在临界区内突然工作、在临界区外无法工作的代码段。然后查看变量,检查缺陷是否明显。如果这样做仍然不能凑效,则设置英特尔® C++ 编译器特定环境变量 KMP_LIBRARY=serial,尝试对整个程序进行设置使之按顺序运行。
如果代码仍然不能工作,则不使用 /Qopenmp 对其进行编译,以确保系列版本能够正常工作。
如果您仍然找不到缺陷的所在,原因可能是您使用了过多的代码。请尝试寻找二进制。通过在并行结构上使用 if(0) 或完全注释掉编译指令来强制并行部分再次连续。另一个方法是强制大部分并行区域成为临界区。挑选一个您认为包含缺陷的代码区域,将其置于临界区内。试着找出在临界区内突然工作、在临界区外无法工作的代码段。然后查看变量,检查缺陷是否明显。如果这样做仍然不能凑效,则设置英特尔® C++ 编译器特定环境变量 KMP_LIBRARY=serial,尝试对整个程序进行设置使之按顺序运行。
如果代码仍然不能工作,则不使用 /Qopenmp 对其进行编译,以确保系列版本能够正常工作。
英特尔® 线程检测器
英特尔® 线程检测器既不是纯粹的调试器,也不是彻底的 lint 工具,它可提供极具价值的并行执行信息和调试提示。英特尔® 线程检测器利用源代码或二进制替换和测试覆盖插入,对 OpenMP 编译指令、Win32 线程化应用编程接口和所有内存访问进行监控,尝试识别代码错误。它可以找到在测试中不常发生,但在客户现场时常出现的错误。请务必谨记在访问尽可能少的内存时,使用该工具测试所有代码路径,以便加快数据收集进程。在通常情况下,您需要对源代码或数据集稍作修改,以减少应用处理的数据量。
英特尔® 线程检测器是为英特尔® VTune™ 性能分析器提供的一个插件,请访问 http://www.intel.com/cd/software/products/apac/zho/index.htm 获取相关信息。
英特尔® 线程检测器是为英特尔® VTune™ 性能分析器提供的一个插件,请访问 http://www.intel.com/cd/software/products/apac/zho/index.htm 获取相关信息。
性能
OpenMP 线程化应用的性能主要取决于以下几点:
一旦算法就位,则应确保代码在英特尔® 架构上高效运行,而单线程版本将会助益良多。关闭 OpenMP 编译器选项,您就能生成一个单线程版本,并可通过常用优化集运行该版本。Richard Gerber 所著的软件优化指南
(在技术书籍销售处有售)为单线程优化提供了有益的参考。一旦您获得了单线程性能,您就可以开始生成多线程版本,并进行一些分析了。
首先,请查看操作系统的闲置循环所花费的时间。英特尔® VTune 性能分析器是协助进行此项调查的一个得力工具。闲置时间可以指示不平衡的负载、大量受阻同步和连续区域。解决这些问题后,返回 VTune 性能分析器,寻找过量的高速缓存未命中和错误共享等内存问题。解决这些基本问题后,您将拥有一个优化的并行程序,它将在超线程技术以及多个物理 CPU 上良好运行。
听起来简单,实际上却很神奇,不是吗?优化的确需要耐心、不断的试错和实践。制作一些小的测试程序来模拟您的应用使用电脑资源的方式,可帮助您了解哪些任务处理起来更快。请务必在并行部分尝试不同的调度子句。与计算时间相比,如果并行区域的开销很大,您可使用下述例子中的 if 子句来按顺序执行该部分。
- 单线程代码的潜在性能。
- CPU 占用率、闲置线程和负载平衡不足。
- 并行执行的应用的比例。
- 线程之间的同步和通信数量。
- 创建、管理、销毁和同步化线程所需的开销因 fork-join 转换(从单线程到并行线程或从并行线程到单线程的转换)的次数的增加而增加。
- 内存、总线带宽和 CPU 执行单元等共享资源的性能限制。
- 由共享内存或错误共享内存引起的内存冲突。
一旦算法就位,则应确保代码在英特尔® 架构上高效运行,而单线程版本将会助益良多。关闭 OpenMP 编译器选项,您就能生成一个单线程版本,并可通过常用优化集运行该版本。Richard Gerber 所著的软件优化指南
(在技术书籍销售处有售)为单线程优化提供了有益的参考。一旦您获得了单线程性能,您就可以开始生成多线程版本,并进行一些分析了。首先,请查看操作系统的闲置循环所花费的时间。英特尔® VTune 性能分析器是协助进行此项调查的一个得力工具。闲置时间可以指示不平衡的负载、大量受阻同步和连续区域。解决这些问题后,返回 VTune 性能分析器,寻找过量的高速缓存未命中和错误共享等内存问题。解决这些基本问题后,您将拥有一个优化的并行程序,它将在超线程技术以及多个物理 CPU 上良好运行。
听起来简单,实际上却很神奇,不是吗?优化的确需要耐心、不断的试错和实践。制作一些小的测试程序来模拟您的应用使用电脑资源的方式,可帮助您了解哪些任务处理起来更快。请务必在并行部分尝试不同的调度子句。与计算时间相比,如果并行区域的开销很大,您可使用下述例子中的 if 子句来按顺序执行该部分。
#pragma omp parallel for if(NumBytes > 50) |
由于本系列白皮书均关于 OpenMP,因此本文暂且不能为您提供一个优化性能的完整且详细的方法。如果您想了解更多性能优化方面的知识,请访问我们英特尔® 软件网络网站上的其它文章。我已经将自己喜欢的几篇文章附在本文末尾的参考资料里,希望能够对您有所帮助。
英特尔® 线程分析工具
最后,我要提一下英特尔® 线程分析工具。与英特尔® 线程检测器一样,它是英特尔® VTune 性能分析器的一个插件,可以通过图形直观显示出某项应用的线程性能状况。它可显示出花费在并行部分、连续部分、开销、同步化及其它多个方面的时间。它能够用性能测量和监控指令替代 OpenMP 编译指令来记录数据。如欲下载英特尔® 线程分析工具和英特尔® VTune 性能分析器,请访问 http://www.intel.com/cd/software/products/apac/zho/index.htm。
总结
OpenMP 由一系列灵活、简单的编译指令、函数调用和环境变量组成,用来明确指导编译器对您的应用进行线程化处理的方式和位置。只要充分利用 OpenMP 的功能,线程化编程将和单线程编程一样轻松。最后,祝您的线程化编程之路一帆风顺!
如欲获取更多关于 OpenMP 的信息,请访问 http://www.openmp.org*
了解 OpenMP 规范。
如欲获取更多关于 OpenMP 的信息,请访问 http://www.openmp.org*
了解 OpenMP 规范。相关资源
同系列其它文章
相关主题
相关开发人员中心

相关主题
相关开发人员中心
- 线程化
- 超线程(HT)技术

- “For 循环”线程化方法
- 准备迎接超线程技术之第 1 部分:改变思维定势(作者:Andrew Binstock)。
- 准备迎接超线程技术之第 2 部分:循环中的线程同步(作者:Andrew Binstock)。
- 线程化应用和英特尔® VTune™ 性能分析器:如何合理地查看应用的各种线程(作者:Jeff Andrews)。
- 利用英特尔® VTune™ 性能分析器优化超线程技术(作者:Richard Gerber)。
- 线程化方法:原则与实践(多名作者)

