借助英特尔® 编译器实现自动并行化 (PDF 242KB)
摘要
通过多线程化应用来提高性能是一件十分耗时的工作。 对于多数计算在简单循环内执行的应用来说,英特尔® 编译器可以自动生成多线程化的版本。
除了高水平的代码优化,英特尔编译器通过自动并行化和OpenMP*功能支持多线程技术。 借助自动化并行功能,编译器可检测能够以并行的方式安全、高效地执行的循环,并生成多线程代码。 OpenMP 支持编程人员通过编译器指令和 C/C++ 编译指令表达并行性。
本文是“英特尔多线程应用开发指南”系列的一部分,该系列介绍了针对英特尔® 平台开发高效多线程应用的指导原则。
背景
英特尔® C++ 和 Fortran 编译器能够分析循环中的数据流,以确定哪些循环可以并行的方式安全、高效地执行。 在多核系统上,自动化并行处理有时可能会导致缩短执行时间。 此外,它还可在以下几方面减轻编程人员的负担:
- 寻找适合并行执行的循环
- 执行数据流分析以确定正确的并行执行
- 手动添加并行编译指令。
编程人员唯一要做的就是向编译命令中添加-Qparallel (Windows*) 或 -parallel (Linux* 或 Mac OS* X)参数。 但是,成功的并行化取决于下一部分介绍的特定条件。
下面的 Fortran 程序包含一个具有较大迭代计数的循环:
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
数据流分析确认该循环不包含数据相关性。 编译器生成的代码在运行时尽可能在线程内平均划分迭代。 线程数默认为逻辑处理器内核或硬件线程的总数(该数值可能大于部分处理器类型的物理内核总数),但是可以通过 OMP_NUM_THREADS环境变量单独设置。 面向特定循环的并行加速取决于工作负载数量、线程之间的负载平衡以及线程创建和同步的开销等,但是相对于使用的线程数,通常低于线性加速的数值。 对于整个程序,加速取决于并行与串行计算的比率(参考任意针对阿姆达尔定律的并行计算的教科书)。
建议
编译器要实现循环的并行化,必须符合三个条件: 首先,在进入一个循环前,必须知道迭代的数量,以便可以提前划分工作负载。 例如,通常不能并行执行 while 循环。 其次,不能发生跳进或跳出循环的情况。 最后,也是最重要的,循环迭代必须是独立的。 换句话说,正确的结果不能在逻辑上依赖迭代执行的顺序。 但是,在累计舍入误差中可能包含微小变化,例如,当以不同的顺序添加相同的数量。 在一些情况下,例如数组或其它临时标量使用的求和,编译器可通过简单的转换去掉明显的相关性。
指针或数组参考的潜在别名是安全并行化的另一个常见障碍。 指向同一个内存位置的两个指针将被赋予别名。 编译器可能无法确定两个指针或数组参考是否指向同一个内存位置,例如,如果它们依靠函数参数、运行时数据或复杂计算的结果。 如果编译器不能证明指针或数组参考的安全性和迭代的独立性,它将不能实现循环并行化,除非认为值得生成备用代码路径,以便在运行时对别名进行明确的测试,但这种情况非常少见。 如果编程人员认为某个特定循环的并行化是安全的,并且可能的别名可以忽略,则可以通过 C 编译指令或 (#pragma parallel) Fortran 编译指令(!DIR$ PARALLEL) 将这种情况通知编译器。 编程人员可以不用更改源代码,借助 -fargument-noalias (Linux 或 Mac OS X) 或 /Qalias-args- (Windows) 进行编译,可声明函数参数是独立的,以及阵列参数不重叠。 (这对 Fortran(而非 C/C++)来说是默认的)。 在 C 中确定指针没有被赋予别名的另一种方式是在指针声明中使用严格的关键字,同时使用-Qrestrict (Windows) 或 -restrict (Linux 或 Mac OS* X) 命令行参数。 但是,如果循环被证明是不安全的,编译器不会对其进行并行化。
编译器只能有效地分析结构相对简单的循环。 例如,它不能确定包含外部函数调用的循环的线程安全性,因为它不知道该函数调用是否造成引入相关性的副作用。concurrency_safe属性可用于英特尔 C++ 编译器,以声明该函数对并行执行是安全的,不会有意料之外的副作用,或者多次函数调用之间不会出现内存访问冲突。 在 C 或 Fortran 中的另一种方法是通过 -Qipo (Windows) 或 -ipo(Linux 或 Mac OS X) 编译器参数调用内部程序优化。 这种方法使编译器有机会针对副作用内联或分析所调用的函数。 Fortran 90 编程人员可以使用 PURE属性来确定子例程和函数不会造成副作用。 而且 DO CONCURRENT结构(源自 Fortran 2008 标准)也可用于声明循环对并行执行是安全的,尤其是PARALLEL或IVDEP:LOOP指令。
当编译器无法自动并行化编程人员认为可以安全、并行执行的复杂循环时,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'可生成下列信息:
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.
对于 k 是否等于 -1 的例子,由于编译器不知道 k 的值,因此它必须假设迭代之间相互依赖。 不过,由于对应用的了解,编程人员可能知道该值(例如 k 总是大于 10000),并可通过插入下面的编译指令忽略编译器:
void add (int k, float *a, float *b) { #pragma parallel for (int i = 1; i < 10000; i++) a[i] = a[i+k] + b[i]; }
此信息表明该循环得到并行化:
procedure: add test.c(6): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED.
但是,编程人员调用此函数时,k的值必须大于 10000,以避免可能的错误结果。
其它编译器特性
版本 12.0 的英特尔编译器包含其他一些与自动并行化相关的特性:
-guide-par (/Qguide-par)与-parallel (/Qparallel)结合使用,因此编译器能够生成建议信息,建议编程人员可帮助编译器自动并行化处理相应循环的方式。 不会生成对象文件。
-par-runtime-control (/Qpar-runtime-control)会使编译器在符号循环界限上生成运行时校验,以决定并行执行循环是否物有所值。 参数可确定校验的程度。
-par-schedule (/Qpar-schedule)规定用于线程之间共享工作的调度算法。 static、dynamic、guided、runtime 等选项与 OpenMP SCHEDULE子句中的类似。
-opt-matmul (/Qopt-matmul) At -O2或更高,可支持编译器识别矩阵乘法循环嵌套或内联函数,并将其替换为调用优化、线程化的库函数,从而提升性能。 如果设置了 -O3和 -parallel (/Qparallel)选项,那么该选项为默认选择。
更多有关这些选项以及其他编译器选项的详细信息,请参阅英特尔编译器 XE 用户与参考指南。
使用指南
尝试使用 -parallel (Linux or Mac OS X) 或 -Qparallel (Windows) 编译开关构建应用的计算密集型内核。 使用 -par-report3 (Linux) 或 -Qpar-report3 (Windows) 提供报告,以便找出并行化的循环与不能并行化的循环。 对于后者,尝试消除数据相关性和/或帮助编译器消除可能具有别名的内存参考的歧义,或通过编译 -guide-par向编译器寻求建议。 通过 -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 处理器的系统上高效利用并行性