现代高性能计算机由下列资源组合构建而成:多核处理器(/zh-cn/articles/frequently-asked-questions-intel-multi-core-processor-architecture#_Essential_concepts)、众核处理器(http://goparallel.sourceforge.net/ask-james-reinders-multicore-vs-manycore/)、大型高速缓存,高带宽进程间通信结构和高速 I/O 功能。 高性能软件需经过设计,以充分利用这些丰富的资源。 无论是重新构建并/或调优现有应用以发挥最高性能,或为现有或未来设备构建新应用,了解编程模型和高效利用资源之间的相互作用极其关键。 以此为起点,全面了解代码现代化(/zh-cn/moderncode)。 关于性能,您的代码至关重要!
构建软件的并行版本可使应用在更短的时间内运行指定的数据集,在固定时间内运行多个数据集,或运行非优化软件禁止运行的大型数据集。 并行化的成功通常通过测量并行版本的加速(相对于串行版本)来进行量化。 除了上述比较之外,将并行版本加速与可能加速的上限进行比较也十分有用。 通过阿姆达尔定律和古斯塔夫森定律(/zh-cn/articles/predicting-and-measuring-parallel-performance)可以解决这一问题。
出色的代码设计将几个不同层面的并行化均考虑在内。
- 第一层并行化是矢量并行化(代码内),在大型数据块上执行相同的计算指令。 代码的标量部分和并行部分都将受益于高效的矢量计算。
- 第二层并行化是线程并行化,其主要特点是单个进程的多条合作线程通过共享内存进行通信,共同处理某项指定任务。
- 第三层并行化是以独立合作进程的方式开发多个代码时,各代码之间通过消息传递系统进行通信。 这种被称为分布式内存队列并行化,之所以如此命名,是因为每个进程指定一个独有的队列号。
开发能够高效使用这三层并行化,并具备高性能的代码是实现代码现代化的最佳选择。
综合考虑这几点会对设备的内存模式产生积极的影响:主内存容量与速度(https://software.intel.com/zh-cn/articles/detecting-memory-bandwidth-saturation-in-threaded-applications)、与内存位置相关的内存访问时间、高速缓存容量与数量,以及内存一致性要求。
矢量并行化时,如果出现数据不对齐,会严重影响性能。 数据应以高速缓存友好型方式(/zh-cn/articles/optimize-data-structures-and-memory-access-patterns-to-improve-data-locality)进行整理。 如果不这样,当应用请求不在高速缓存内的数据时,性能将会下降。 当所需的数据在高速缓存内,内存访问速度会达到最快。 高速缓存之间的数据传输均以高速缓存行进行,因此,如果下一组数据不在当前高速缓存行内,或分散于多个高速缓存行,应用的高速缓存效率会降低。
除法和超越数学函数非常昂贵,即使指令集直接支持这些函数。 如果您的应用在运行时代码内使用多项除法和平方根运算,因为硬件内的功能单元有限,性能会有所降低;连接这些单元的管道可能会占主导。 由于这些指令非常昂贵,开发人员希望高速缓存使用频率较高的值,以提升性能。
“一刀切”的技术不存在。 人们太过于依赖正在处理的某个问题和对代码的长期要求,但优秀的开发人员会关注不同层面的优化,不仅满足当前需要,还会满足未来需求。
英特尔构建了一套完整的工具来协助代码现代化,包括编译器、资源库、调试器、性能分析器,并行优化工具等等。 此外,作为并行计算机开发领域的领导者,英特尔以其超过三十年的丰富经验为基础提供网络研讨会、文档、培训示例,以及最佳方法和案例研究。
面向多层并行的代码现代化 5 阶段框架
代码现代化(http://software.intel.com/moderncode)优化框架以系统化方式进行应用性能优化。 该框架将应用分为 5 个优化阶段,各阶段相互作用,相互影响,以共同提升应用性能。 但是,启动优化流程之前,您应考虑应用是否需要重新构建(根据以下指南)以实现最高性能,然后按照代码现代化优化框架进行优化。
借助该优化框架,应用可在英特尔® 架构上实现最高性能。 这种分布式方法有助于开发人员在最短的时间内实现最高的应用性能。 换句话说,它支持程序在执行环境中最大限度地使用所有的并行硬件资源。 这 5 个阶段分别为:
- 利用优化工具和库:使用英特尔® VTune™ Amplifier 分析工作负载,以确定热点,并使用英特尔® Advisor XE(https://software.intel.com/zh-cn/intel-advisor-xe/)识别矢量化和线程化机会。 使用英特尔编译器生成最佳代码,并在适当的情况下运用英特尔® 数学核心函数库(/zh-cn/articles/parallelism-in-the-intel-math-kernel-library)、英特尔® TBB(/zh-cn/intel-tbb) 和 OpenMP*(http://openmp.org/wp/) 等优化的资源库。
- 标量串行优化:保持正确的精度,输入常量,并使用合适的函数和精度标记。
- 矢量化:利用 SIMD 特性以及数据布局优化。采用高速缓存对齐的数据结构,将结构数组转化为数组结构(/zh-cn/articles/a-case-study-comparing-aos-arrays-of-structures-and-soa-structures-of-arrays-data-layouts),并最大限度地减少条件逻辑。
- 线程并行化:分析线程扩展,并将线程与内核关联。 扩展问题通常是由于线程同步或内存利用率低下所造成的。
- 将应用从多核扩展到众核(分布式内存队列并行化):扩展对高度并行化应用来说极为重要。 在将执行对象从一种偏爱的英特尔架构(英特尔® 至强™ 处理器)换至另一种(英特尔® 至强融核™ 协处理器)的过程中,最大限度地减少变化并最大限度地增强性能。
代码现代化 – 5 阶段的实际运用
第 1 阶段
在开始优化项目时,您需要选择一个优化开发环境。 该选择对于后续步骤具有重要的影响。 它不仅会影响您得到的结果,还能大幅减少您的工作量。正确的优化开发环境可以为您提供出色的编译器工具、现成的优化库、调试工具和性能评测工具,帮助您准确地查看代码在运行时正在做什么。 查看英特尔® Advisor XE(https://software.intel.com/zh-cn/intel-advisor-xe-support/training)工具中的网络研讨会,并以此识别矢量化和线程化机会。
第 2 阶段
用尽了可供使用的优化解决方案后,如果还要发挥应用的更高性能,您需要启动与应用的源代码相关的优化流程。 在开展活动并行编程之前,您需要确保应用在进行向量化和并行化处理之前可提供正确的结果。 同样重要的是,您需要确保应用能够以最少的运算得到正确的结果。 您要考虑数据和算法相关的问题,如:
- 选择合适的浮点精度
- 选择合适的估算法准确度:多项式或有理数
- 避免跳跃算法
- 利用迭代计算缩短循环运算长度
- 避免或最大程度减少算法中的条件分支
- 避免重复计算,使用之前的结果
您还必须处理语言相关的性能问题。 如果您使用的是 C/C++,与该语言相关的问题包括:
- 对所有常量使用外显式型态法 (explicit typing),以避免自动升级
- 选择正确的 C 运行时函数类,比如 doubles 或 floats:
exp()
与expf()
;abs()
与fabs()
- 以显性方式将点别名告知编译器
- 显式调用内联函数,以避免开销
第 3 阶段
尝试矢量级并行化。 首先尝试对内层循环进行矢量化。 为了获取高效的矢量循环,请确保控制流分散达到最少,以及内存访问保持一致。 外层循环矢量化是一种用于增强性能的技术。 默认情况下,编译器会对嵌套循环结构中最内层的循环进行矢量化处理。 但在某些情况下,最内层循环中的迭代数量较小。 此时,对最内层循环进行矢量化有些得不偿失。 但是,如果外层循环中具有更多的工作,则可以使用一个基本函数组合(strip-mining 和编译指示/指令 SIMD)在外层循环强制执行矢量化操作,以实现更好的效果。
- SIMD 在“封包”和对齐的输入数据上表现最为出色,但由于其本身的性质,它会对控制分散造成不利影响。 此外,如果应用实施专注于数据邻近度,现代硬件会实现出色的 SIMD 和线程性能。
- 如果内层循环没有足够的工作(例如,运行次数非常低;矢量化的性能优势可以测量),或数据依赖性妨碍针对内层循环的矢量化,请尝试对外层循环进行矢量化。 外层循环可能会产生控制流分散;尤其是在内层循环的运行次数由于外层循环每个迭代的不同而有差异的情况下。 这样会限制通过矢量化而实现的性能改进。 外层循环的内存访问可能与内层循环不同。 这样会造成收集/分散指令(而非矢量加载和存储),从而大大限制通过矢量化而实现的扩展。 数据转换(比如转置二维数组)可缓解这些问题,或尝试将结构数组转化为数组结构(https://software.intel.com/zh-cn/articles/a-case-study-comparing-aos-arrays-of-structures-and-soa-structures-of-arrays-data-layouts)。
- 由于循环层级较浅,上述指南可能会造成需要同时对循环进行并行化和矢量化处理。 在这种情况下,该循环不仅需要提供足够的并行工作以弥补开销,还要维持控制流均匀性和内存访问一致性。
- 更多详情请查看矢量化要素(/zh-cn/articles/vectorization-essential)。
第 4 阶段
现在我们要进行线程级并行化处理。 确定最外层,并尝试对该层进行并行化处理。 显然,这要求维护潜在数据竞跑,并在需要时将数据声明移到循环内部。 它还要求以高效利用高速缓存(/zh-cn/articles/avoiding-and-identifying-false-sharing-among-threads)的方式维护数据,以降低跨多条并行路径进行数据维护所产生的开销。 之所以对最外层进行并行化处理,是希望为每条独立线程提供尽可能多的工作。 阿姆达尔定律表明: 使用多台处理器的程序在并行计算过程所实现的加速受制于程序顺序片段所需的时间。 由于工作量需要用来弥补并行化所产生的开销,因此有利于每条线程拥有尽可能多的并行工作。 如果由于不可避免的数据依赖性导致最外层无法实现并行化,请尝试对能够正确实现并行化的下一个最外层进行并行化处理。
- 如果最外层的并行工作量能够满足目标硬件的需要,并能随并行资源的合理增加而进行扩展,那么您已达到并行化处理的目标。 请勿进行其他并行化处理,因为这样会显著增加开销(线程控制开销将否定一切性能改进),也无法实现任何性能提升。
- 如果并行工作仍然不够,例如,经过内核扩展测试,最多只能扩展到少量内核,而无法扩展到实际内核数,请尝试对其他层(尽量为最外层)进行并行化处理。 请注意,您无需将循环层级扩展到所有可用内核,因为可能有其他循环层级处于并行执行之中。
- 如果无法在第 2 阶段生成可扩展代码,原因可能是算法中的并行工作不够。 这表示,划分多条线程之间的固定工作量使每条线程得到的工作量极少,因此启动和终止线程所产生的开销抵消了有用工作。 也许算法能够进行扩展以处理更多工作,例如,尝试处理更大的问题。
- 请确保您的并行算法能够高效利用高速缓存。 如果不是,请将该其重新设计成高速缓存高效型(/zh-cn/articles/optimize-data-structures-and-memory-access-patterns-to-improve-data-locality)算法,因为这种算法不随并行化而扩展。
- 更多详情敬请查看英特尔多线程应用开发指南(/zh-cn/articles/intel-guide-for-developing-multithreaded-applications)系列。
第 5 阶段
最后我们要做的是多节点(队列)并行化。 许多开发人员认为,消息传递接口 (MPI) 就是“仅运行”于场景背后的黑匣子,将数据从一项 MPI 任务传输到另一项。 对于开发人员来说,MPI 的魅力在于其算法编码独立于硬件。 而开发人员担忧的是,由于众核架构采用 60 多个内核,任务之间的通信可能会在单个节点内部或跨节点产生通信风暴。 为了缓解这些通信瓶颈,应用可采用混合技术,混合使用几项 MPI 任务和几条 OpenMP 线程。
- 更多详情敬请查看使用英特尔® MPI 实现并行化(/zh-cn/articles/parallelization-using-intel-mpi)。
经过良好优化的应用可处理矢量并行化、多线程并行化和多节点(队列)并行化。 然而,为了高效完成这些并行化,可使用标准分布式方法,以确保兼顾到各阶段的层面。 根据个独立应用的特定需求,可以(通常)对上述阶段进行重新排序;您可以在某一阶段迭代两次以上,以实现预期性能。
根据我们的经验,必须实施所有阶段,以确保应用不仅能够在目前的可扩展硬件上实现出色性能,还可在未来硬件上进行有效扩展。
试试看!