通往多核高速路的入口:为代码并行化做准备

文章下载

下载通往多核高速路的入口:为代码并行化做准备(英文版) [PDF 143KB]


如果没有适当的准备,代码并行化不仅不会提高性能,还可能会降低性能。 为代码并行化做准备需要遵守哪些原则?我们来听听专家们的意见。

作者 Andrew Binstock

编写游戏和数据可视化软件时,开发人员通常会在线优化代码。也就是说,一边编写代码,一边稍稍调整代码设计和实施--在这里更改数据类型,在那里加固数据结构,先执行功能 B 再执行功能 A,等等。应避免为提升性能而做此类修改--修改目的可能与大多数开发人员的主要使命不一致:编写和交付可靠的功能。在代码正常运行时,对代码进行系统的优化和并行化才有意义。

本文讨论了为什么不鼓励边写代码边做优化,以及性能专家们是如何实现性能提升的。文章介绍了专家为了不做无用功,不干扰正常运行的代码,如何针对优化有系统地准备代码以及如何完成优化过程。


功能与性能

在软件开发中需要遵守的一条安全原则是,如果有一位杰出的计算机科学家建议不要做某件事情,你应该考虑采纳他的建议。如果有两位杰出的计算机科学家反对同一件事,而你没有接受他们的建议,你应当承担由此带来的全部责任。 有两位杰出的计算机科学家说过: “过早优化是万恶之源。” 这个座右铭最早出自 Quicksort 发明者 Tony Hoare 之口,后来被 Donald Knuth 引用而广为流传。(因此,人们常常认为这是 Knuth 的座右铭。)

在 Hoare 和 Knuth 时代,为提升速度而优化代码几乎是不可避免的,因为当时所有的硬件都非常慢。采用可能会减少处理器周期或降低 I/O 速度的捷径,有时是确保按时完成任务的唯一有效方法。

40 年后,我们发现今天的系统具备超强动力,性能需求与按时完成任务不再相关。更确切的说,正如我们在游戏行业中所看到的,性能是赢得竞争的必要条件。在市场上,性能越高,硬件利用率越高,游戏售价就越高。考虑到竞争激烈的游戏市场,性能优势是成败与否的关键。压力太大,不得不进行优化!

不幸的是,在巨大压力的推动下,开发人员和经理不得不采用 Knuth 和 Hoare 极力反对的方法--只为提升速度而编写垃圾代码。每次在正确实施特性之前对软件进行优化或并行化,都会带来巨大的损失。这些损失包括什么?

1) 降低代码可读性。优化必然会改变算法和算法实施。结果,正确实施算法,尤其是在代码编写十分困难的并行环境中,会让任务变得更加复杂。另外,调试也会更加困难,因为很难确定非介入式代码正在做什么。

2) 代码维护更加困难。原始开发人员后面的程序员会发现,经过优化的代码更难读取。即便是简单的变动也会变成后续开发人员不愿触碰的代码片段。通常,此类代码会被永远丢弃。 即使代码真使性能降低了,如果开发人员不确定代码正在做什么,他也不会触碰这个代码,以免破坏了某项特性。结果,不恰当的优化或并行化会让代码变得不可维护。

3) 通常不会改善性能。当性能工程师准备调试代码时,他们首先要分析代码的性能。他们不会凭借自己丰富的优化经验来猜测需要对哪些代码进行并行处理或者调试。因此,开发人员也不应这样做。仔细评估是确定哪些代码需要优化以及如何优化的唯一安全的方法。 选错了并行处理的代码或者并行处理不够专业,都会降低性能。

那么,如何对代码进行并行处理和优化呢?


先正确优化, 然后再并行处理。

不要急着修改!杰出的计算机科学家说过:

先求准,再求快。 先把代码弄干净,再让它变快。 提高速度时要保持代码的正确性。 -Kernighan 和 Plauger,《编程风格要素》

过早优化是万恶之源。- Donald Knuth,引自 C. A. R. Hoare

性能的关键是精确,不是大量的特殊案例。避免糟糕的调整。- Jon Bentley 和 Doug McIlroy

这些原则可以归纳为以下几点: "1. 不要过早优化。 2. 在你知道需要优化之前不要优化。 3. 在你知道哪些代码需要优化以及在哪里进行优化之前不要优化。” -Herb Sutter
第一步也是最重要的一步是,让代码正确运行。这意味着,确保算法正确,按标准分别测试各个算法,确保它可以按照用户要求运行,并且符合相应的规范。在多数情况下,要求就是编写串行代码,确保它作为串行代码的正确性。

因为在后面的优化和并行化当中,你需要验证你没有更改代码结果,你需要编写大量的单元测试,甚至是功能测试。在并行化代码上运行这些测试可以让你相信,优化没有扰乱你先前验证的实施。

确保代码可以正确运行并且准备好单元测试后,你仍然还需要做其它准备。接下来,你需要确保代码中没有错误,例如内存漏洞。 如果很难发现常规代码中的内存漏洞,那么在线程代码中就更难发现。与几年前相比,现在调试线程代码简单多了,即便这样,依然很难清理单线程代码。因此,这就是查找内存漏洞、过渡内存消耗等问题的意义所在。

总之,在开始修改代码之前,你希望代码尽可能干净。最后一项值得做的检查是使用 Intel® Parallel Inspector(/en-us/intel-parallel-inspector/),确保代码没有错误或者不适当的设计,它们会给并行实施带来许多问题。(图 1)该工具是 Microsoft Visual Studio* 的插件,可以在 C/C++ 代码上运行,是准备代码并行化的手动方法。 注意:虽然 Intel Parallel Inspector 是一款独立工具,但是它还可以与 Intel® Parallel Studio(/en-us/intel-parallel-studio-home/)、Intel® Parallel Amplifier(并行性能分析)以及Intel® Parallel Composer(并行编译器和调试器)绑定。稍后,我会详细介绍这个产品。

图 1. Intel® Parallel Inspector 中的窗口,显示了一个锁定问题(顶部)和详细信息(底部)。


哪些代码需要并行化

现在,我们知道代码是好的,没有负面影响,而且通过了几项单元测试,我们来了解一下需要对哪些内容进行并行化。 一开始可能希望尽可能多的进行并行化:将代码分解成几个可以运行的字符串--功能分解--然后并行运行这些字符串。 接下来,处理大量的数据(例如渲染),对这些数据进行并行处理(数据分解)。在执行其中一项任务时,你可能认为你可以让 CPU 核心和 CPU 引擎在最大负载下运行,因此会把机器变成一个吞食了大量代码的野兽。这个想法不错,但是与现实情况不符。

首先,今天的处理器核心每秒大约可以执行 25 到 30 亿条指令。在这样的速度下,许多串行执行的任务已经能够很快地运行了,处理器核心在大部分时间的占用率远不足 100%。 因此,不会有大量的代码可以从并行处理中获益(如果有)。

因为并行处理和优化都是代价高昂的任务--值得注意的是,更改代码需要很长的时间,还会增加调试和代码维护的难度--只有在真正获得性能优势时,才有必要对代码进行并行处理。为找到此类代码,需要分析软件的性能。

性能分析工具,例如 Intel Parallel Amplifier(图 2)和Intel® VTune™ Performance Analyzer可以显示性能受限或受阻的热点。

图 2. 英特尔® Parallel Amplifier显示的热点是一系列需要最长执行时间的函数或方法。您可以在左侧面板选择函数,之后将在中间出现执行图,并且在右侧出现调用堆栈。其它面板将展示每条代码语句的执行时间。(来源:英特尔公司)

优秀的性能分析工具可以按照成本降序显示热点(图 2)。此处的成本指相应函数消耗的执行时间。最佳策略是重点研究主要热点,然后根据需要依次处理其它次要热点。在大多数应用中,会有几个热点比其它函数更为突出,因此可以很容易地找到关注的重点。

然而,对于一些程序,要找出主要热点并不容易。一般来说,对消耗总执行时间不到 5% 的函数进行并行处理会得不偿失。也就是说,将总运行时间减少 5% 通常没有明显的性能提升,对于游戏、虚拟化和带有人机界面的其它软件尤其如此。

此外,性能测试还将可以显示出系统资源的使用情况,从而能够帮助了解工作负载在处理资源间的分配情况(如图 3 所示)。

图 3. 英特尔® Parallel Amplifier 展示处理器核心的使用情况。中间面板中彩条的长度展示了核心的利用率,以及运行的函数(左侧面板)。右侧面板中的图形显示函数大多数情况下使用了一个核心。(来源:英特尔公司)

在检查硬件执行时:应特别关注 CPU-GPU 二者的关系。在所有图形密集型软件中,这些处理单元完成的工作需要达到合理搭配,以获得最佳系统性能。因此,分析采用不同图形适配器的系统运行代码的性能非常重要。这将反映出 GPU 对系统性能的正面或负面影响,以及在 GPU 的计算能力发生变化时热点位置和大小的变化。

这些信息非常重要,因为如果热点与 GPU 的性能有关,并行处理将无法解决问题。这时,您需要对图形适配器进行升级或者对整个 GPU 接口进行检查以确定是否有更好的数据流传输方式,可以提升整体性能。

如果根本问题在于 GPU 过慢,难以满足相应需求,您需要更全面地重新评估优化问题,而不仅仅是解决执行热点。正如之前所述,理想情况下 CPU 和 GPU 将达到合理搭配。对于高端 CPU 加低端 GPU 的方案,由于 CPU 几乎一直处于等待状态,将导致性能低下。如果二者关系互换,由于 GPU 一直处于等待状态,因此性能也将非常低。在这两种情况下,性能都远未能达到最佳。(如欲了解有关此问题的更多信息,请参阅 Ben Garney 和 Eric Preisz 共同编写的《视频游戏优化(Video Game Optimization)》一书。http://is.gd/eQcwt


哪个热点?

在确定成本最昂贵的热点并且理解 GPU 的作用后,您可以开始重点研究代码。

如果某个热点显得特别大,它将成为首要目标。然而,在接触代码之前,您应检查该热点函数,确保其适合进行并行处理。本网站的其它文章讨论了功能和数据分解,在进行分析前您应理解这些方法。您面临的最简单的问题是:哪些函数可以进行并行处理?如果处理的是大型数据块(这在图形软件中很常见),通常可以将这些数据块分解为较小的块,然后可以使用单独的线程处理各个小块,各个线程都执行相同的功能。我们将在本系列的后续文章中讨论数据块的大小和线程的数量,然而根据以往的经验,线程和数据块的数量都可以灵活些。您可以通过反复分析软件性能并且查看其效果来确定最佳数量。然而,在针对具体硬件配置进行优化时潜伏着一定的危险。软件无疑会运行在核心数量各异的多种平台上,因此应该粗略地决定如何分配数据,而不是针对特定设置对并行化进行调试。

有时,热点并没有处理大量数据。相反,它是在处理多种不同任务,其中有些非常缓慢。在这种情况下,主要的解决方法是查看缓慢任务中哪些可以并行运行。理想情况下,这些任务是独立的缓慢任务,或者具有易控制的相关性。例如,磁盘 I/O 是一种非常缓慢的操作,无法从程序的角度显著改善性能。然而,您可以创建一个任务,在读取磁盘数据的同时开始进行处理,而不必等到读出所有数据后再开始处理。这种方法不会减少缓慢任务消耗的时间,但可以提高在此期间完成的工作量。如果缓慢任务成为导致热点的主要原因,而且无法对其进行彻底并行处理,则可以重新考虑设计。以磁盘 I/O 为例,能否缓存数据以减少对 I/O 的需要?能否对数据进行安排,以便首次读取能够提供有关处理任务的足够信息,使任务能够并行分开处理等?

有时,您会发现您检查的热点无法进行并行处理。如果是这样,只能接受这种情况,继续检查。您应该检查所有主要的热点,以确定对它们进行并行处理的难易程度。通常,最好从容易的热点开始。一个很重要的经验是,解决最容易处理的热点有时便能够带来足够的性能提升,这时候解决其余的热点将不再那么紧迫,或者它们将不能带来与投入所对应的提升。然而,这些热点需要在对所有主要热点完成检查和评估之后才能够确定。


最后一个小建议

在下一篇文章中,我将介绍如何对热点进行并行处理。同时,我还要分享一个有用的小建议。笔记本是处理这种工作不可或缺的工具。它可以是一个在线笔记本,或者一个物理文件夹。并行代码会产生许多值得记录的重要数据。例如,在多台机器上分析代码以评估 GPU 的效果将产生大量值得记录的数据。在您考虑各种优化方案时,您会需要参考它们。

电子表格是记录数字的一种简单方法,但我发现 wiki 中的简单表格更好些,而且可以轻松共享。此外,还可以在笔记本上记录有关并行处理热点适用性的注释和观察。您观察到了什么?哪些有效果?哪些效果不明显?这些信息由两方面的用处:1) 它为以后的优化留下了依据,2) 有助于您发现热点的相似之处,并且可能发现能够改进多个热点的变化。在编码过程中进行记录的原则正在逐渐消失,但在优化过程中,记录将可以显著减少花费在反复操作上的时间。如果您最终采用电子形式的笔记本,请务必记录源代码管理系统。


致谢

我非常感谢Paul Lindberg很有思想的评论和大量实用的建议。


作者简介

Andrew Binstock 现任 Pacific Data Works LLC 首席分析师。他是《软件开发时代》(SD Times)的专栏作家,并且是《InfoWorld》杂志的高级特约编辑。他已经为英特尔出版社撰写了两本有关编程的书籍。在写作间隙,他为排版软件开源项目 Platypus(http://platypus.pz.org/)编写代码。
Tags:
Para obter mais informações sobre otimizações de compiladores, consulte Aviso sobre otimizações.