摘要
通过多线程化应用来提高性能是一件十分耗时的工作。对于多数计算在简单循环内执行的应用来说,英特尔 ® 编译器可以自动生成多线程化的版本。除了高水平的代码优化,英特尔编译器通过自动并行处理和OpenMP* 功能支持线程化功能。借助自动化并行功能,编译器可检测能够以并行的方式安全、高效地执行的循环,并生成多线程代码。OpenMP 允许编程人员通过编译器指令和 C/C++ 编译指令表达并行性。
本文是《英特尔® 多线程应用开发指南》大型系列的一部分,提供了针对英特尔 ® 平台开发高效的多线程应用的指南。
背景
英特尔 ® C++ 和 Fortran 编译器能够分析循环中的数据流,以确定哪些循环可以并行的方式安全、高效地执行。在多核系统上,自动化并行处理有时可能会导致缩短执行时间。此外,它还可在以下几方面减轻编程人员的负担:- 寻找适合并行执行的循环
- 执行数据流分析以确定正确的并行执行
- 手动添加并行编译指令。
下面的 Fortran 程序包含一个具有较大迭代计数的循环:
数据流分析确认该循环不包含数据相关性。编译器生成的代码在运行时尽可能在线程内平均划分迭代。线程数默认为处理器核心的总数(如果支持英特尔®超线程技术,该数值可能大于物理核心的总数),但是可以通过 OMP_NUM_THREADS 环境变量单独设置。面向特定循环的并行加速取决于工作负载数量、线程之间的负载平衡以及线程创建和同步的开销等,但是相对于使用的线程数,通常低于线性加速的数值。对于整个程序,加速取决于并行与串行计算的比率(参考任意针对阿姆达尔定律的并行计算的教科书)。PROGRAM TEST PARAMETER (N=10000000) REAL A, C(N) DO I = 1, N A = 2 * I - 1 C(I) = SQRT(A) ENDDO PRINT*, N, C(1), C(N) END
建议
编译器要实现循环的并行化,必须符合三个条件:首先,在进入一个循环前,必须知道迭代的数量,以便可以提前划分工作负载。例如,通常不能并行执行 while 循环。其次,不能发生跳进或跳出循环的情况。最后,也是最重要的,循环迭代必须是独立的。换句话说,正确的结果不能在逻辑上依赖迭代执行的顺序。但是,在累计舍入误差中可能包含微小变化,例如,当以不同的顺序添加相同的数量。在一些情况下,例如数组或其它临时标量使用的求和,编译器可通过简单的转换去掉明显的相关性。指针或数组参考的潜在别名是安全并行化的另一个常见障碍。指向同一个内存位置的两个指针将被赋予别名。编译器可能无法确定两个指针或数组参考是否指向同一个内存位置,例如,如果它们依靠函数参数、运行时数据或复杂计算的结果。如果编译器不能证明指针或数组参考的安全性和迭代的独立性,它将不能实现循环并行化,除非认为值得生成备用代码路径,以便在运行时对别名进行明确的测试,但这种情况非常少见。如果编程人员认为某个特定循环的并行化是安全的,并且可能的别名可以忽略,则可以通过 C 编译指令或 (#pragma parallel) Fortran 编译指令 (!DIR$ PARALLEL) 将这种情况通知编译器。在 C 中确定指针没有被赋予别名的另一种方式是在指针声明中使用严格的关键字,同时使用 -Qrestrict (Windows) 或 -restrict (Linux 或 Mac OS* X) 命令行参数。但是,如果循环被证明是不安全的,编译器不会对其进行并行化。
编译器只能有效地分析结构相对简单的循环。例如,它不能确定包含外部函数调用的循环的线程安全性,因为它不知道该函数调用是否造成引入相关性的副作用。Fortran 90 编程人员可以使用 PURE 属性来确定子例程和函数不会造成副作用。在 C 或 Fortran 中的另一种方法是通过 -Qipo (Windows) 或 -ipo (Linux 或 Mac OS X) 编译器参数调用内部程序优化。这种方法使编译器有机会针对副作用内联或分析所调用的函数。
当编译器无法自动并行化编程人员认为可以安全、并行执行的复杂循环时,OpenMP 是首选解决方案。通常,编程人员对代码的理解优于编译器,并能以更大的粒度表达并行性。另一方面,自动并行化针对嵌套循环可能非常有效,例如矩阵相乘中的嵌套循环。粒度大小适中的并行性源于外部循环的线程化,可以使用向量化或软件流水线优化内部循环以获得精细粒度并行性。
可以并行化的循环不代表一定要实现并行化。编译器使用带有阈值参数的成本模型来确定是否应对循环进行并行化。-Qpar-threshold[n] (Windows) 和 -par-threshold[n] (Linux) 编译器蚕食可调整该参数。n 值的范围是 0-100,0 表示始终并行化安全的循环,而不考虑成本模型;100 表示编译器只并行化那些很可能获得高性能的循环。缺省的 n 值被保守地设置为 100;有时候,阈值降到 99 可能会显著增加并行循环的数量。编译指令 #parallel always(在 Fortran 中是 !DIR$ PARALLEL ALWAYS) 可以用于忽略单个循环的成本模型。
开关 -Qpar-report[n] (Windows) 或 -par-report[n] (Linux),其中 n 为 1-3,显示哪些循环得到并行化。查找信息,例如:
如下例所示,编译器还可以报告哪些循环不能并行化以及相应的原因。test.f90(6) : (col. 0) remark: LOOP WAS AUTO-PARALLELIZED
下面的例子对此进行了阐述:serial loop: line 6 flow data dependence from line 7 to line 8, due to "c"
void add (int k, float *a, float *b)
{
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
编译命令 'icl -c -Qparallel -Qpar-report3 add.cpp' 可生成下列信息:对于 k 是否等于 -1 的例子,由于编译器不知道 k 的值,因此它必须假设迭代之间相互依赖。不过,由于对应用的了解,编程人员可能知道该值(例如 k 总是大于 10000),并可通过插入下面的编译指令忽略编译器:procedure: add test.c(7): (col. 1) remark: parallel dependence: assumed ANTI dependence between a line 7 and a line 7. flow data dependence assumed ... test.c(7): (col. 1) remark: parallel dependence: assumed FLOW dependence between a line 7 and b line 7.
void add (int k, float *a, float *b)
{
#pragma parallel
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
此信息表明该循环得到并行化。
但是,编程人员调用此函数时,K 的值必须大于 10000,以避免可能的错误结果。procedure: add test.c(6): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED.
使用准则
尝试使用 -parallel (Linux or Mac OS X) 或 -Qparallel (Windows) 编译开关构建应用的计算密集型内核。使用-par-report3 (Linux) 或 -Qpar-report3 (Windows) 提供报告,以便找出并行化的循环与不能并行化的循环。对于后者,尝试消除数据相关性和/或帮助编译器消除可能具有别名的内存参考的歧义。通过 -O3 编译可实现额外的高级循环优化(例如循环合并),此操作有时候可实现自动并行化。以 -opt-report-phase hlo 生成的编译器优化报告中对此额外优化进行了报告。始终在具有和没有并行化的情况下测量性能,以确定是否实现了加速。如果 -openmp 和 –parallel 在同一个命令行中指定,编译器将只尝试对不包含 OpenMP 指令的循环进行并行化。对于拥有独立的编译和链接步骤的程序,当使用自动并行化功能时要确保链接到 OpenMP 运行时库。最简单的方法就是使用编译器驱动程序进行连接,例如通过icl -Qparallel (Windows) 或ifort -parallel (Linux 或 Mac OS X)。在 Mac OS X 系统上,您可能需要在 Xcode 中设置环境变量 DYLD_LIBRARY_PATH,以确保在运行时可以找到 OpenMP 动态库。其它资源
英特尔 ® 软件网络并行编程社区英特尔® C++ 编译器用户和参考指南或英特尔® Fortran 编译器用户和参考指南中的“优化应用/使用并行性:自动并行化”
在基于奔腾 ® III 和奔腾 ® 4 处理器的系统上高效开发并行功能
Intel ® Parallel Composer中实施并行化的概述
