我从事计算科学方面的工作,结果,我花费了大量的时间来尝试提高许多代码的科学吞吐量,并了解这些代码的效率。
假设我已经评估了我正在开发的软件的性能与可读性/可重用性/可维护性之间的权衡,并且我决定是时候提高性能了。我们还假设我知道我没有针对我的问题的更好算法(就flop / s和内存带宽而言)。您还可以假设我的代码库使用的是C,C ++或Fortran之类的低级语言。最后,我们假设代码中没有并行性,或者我们只对单个内核的性能感兴趣。
首先要尝试的最重要的事情是什么?我怎么知道我可以获得多少性能?
我从事计算科学方面的工作,结果,我花费了大量的时间来尝试提高许多代码的科学吞吐量,并了解这些代码的效率。
假设我已经评估了我正在开发的软件的性能与可读性/可重用性/可维护性之间的权衡,并且我决定是时候提高性能了。我们还假设我知道我没有针对我的问题的更好算法(就flop / s和内存带宽而言)。您还可以假设我的代码库使用的是C,C ++或Fortran之类的低级语言。最后,我们假设代码中没有并行性,或者我们只对单个内核的性能感兴趣。
首先要尝试的最重要的事情是什么?我怎么知道我可以获得多少性能?
Answers:
首先,正如技术人员和Dan所指出的那样,分析是必不可少的。我个人在Linux上使用了英特尔的VTune放大器,因为它为我提供了一个非常细致的概览,说明了花时间在哪里做。
如果您不打算更改算法(即,如果没有重大更改会使您的所有优化过时),那么我建议您寻找一些可能有很大不同的常见实现细节:
内存位置:一起读取/使用的数据是否也存储在一起,还是您在这里零散地捡拾点点滴滴?
内存对齐:您的双打实际上对齐了4个字节吗?你是怎么打包的structs?要学究,请使用posix_memalign代替malloc。
缓存效率:本地性可以解决大多数缓存效率问题,但是如果您有一些经常读取/写入的小数据结构,则它们是缓存行的整数倍或分数(通常为64字节)时,会有所帮助。如果您的数据与高速缓存行的大小对齐,也会有所帮助。这样可以大大减少加载一条数据所需的读取次数。
向量化:不,不要手工编写汇编程序。gcc提供可自动转换为SSE / AltiVec /任何说明的向量类型。
指令级并行性:矢量化的混蛋。如果某些经常重复的计算不能很好地向量化,则可以尝试累加输入值并一次计算多个值。有点像循环展开。您在这里利用的是您的CPU通常每个内核具有一个以上的浮点单元。
算术精度:在做任何事情时,您是否真的需要双精度算术?例如,如果要在牛顿迭代中计算校正,则通常不需要所有要计算的数字。为了更深入的讨论,请参阅该文。
其中一些技巧在daxpy_cvec 此线程中使用。话虽如此,如果您使用的是Fortran(在我的书中不是低级语言),那么您对这些“技巧”中的大多数将几乎没有控制权。
如果您在某些专用硬件(例如,用于所有生产运行的群集)上运行,则可能还需要阅读所用CPU的详细信息。并不是说您应该直接为该体系结构编写汇编程序,而是要激发您找到其他您可能错过的优化方法。了解功能是编写可利用该功能的代码的必要的第一步。
更新资料
自从我写这篇文章已经有一段时间了,我还没有注意到它已经成为一个如此受欢迎的答案。因此,我想补充一点:
我敢肯定,对于许多非计算机科学家来说,这将带回那些导致什么都没有进行的上述学科令人沮丧的讨论的记忆,或者是其他人的轶事的记忆。不要气our。跨学科协作是一件棘手的事情,需要一些工作,但是回报却是巨大的。
以我作为计算机科学家(CS)的经验,诀窍在于获得期望和正确的沟通。
期望 -CS仅在他/她认为您的问题很有趣时才会帮助您。对于您不了解的问题,这几乎排除了尝试优化/矢量化/并行化已编写但未真正注释的一段代码。CS通常对潜在问题更感兴趣,例如用于解决该问题的算法。不要给他们解决方案,给他们解决问题。
另外,准备让CS说“ 此问题已解决 ”,并为您提供参考文献。忠告:阅读该论文,如果确实适用于您的问题,请执行其建议的任何算法。这不是一个自鸣得意的CS,它只是为您提供了帮助。不要生气,记住:如果问题在计算上不是很有趣,即问题已经解决,并且解决方案显示为最优,那么他们将无法解决问题,只需为您编写代码即可。
通信 -wise,记住,大多数的CS是不是在你的领域的专家,并在条款解释的问题是什么,你在做什么,而不是如何和为什么。通常我们真的不关心,为什么,以及如何,没错,是我们最擅长的。
例如,我目前正在与一群计算宇宙学家合作,基于SPH和Multipoles编写其仿真代码的更好版本。花了大约三场会议才能停止谈论暗物质和星系光环(呵呵),并深入到计算的核心,即他们需要找到每个粒子给定半径内的所有邻居,然后计算出一些数量超过它们,然后再次遍历所有所述邻居,并将该数量应用于其他计算。然后移动粒子或至少其中一些粒子,然后再次进行所有操作。您会看到,尽管前者可能非常有趣(它是!),但后者是我开始考虑算法时需要了解的内容。
但是我的主要观点有所不同:如果您真的对快速进行计算感兴趣,而您自己又不是计算机科学家,请与他人讨论。
科学软件与其他软件没有太大区别,就如何知道需要调整的内容而言。
我使用的方法是随机暂停。这是它为我找到的一些加速方法:
如果将很大一部分时间花费在诸如log和等函数上exp,那么我将看到这些函数的自变量是什么,取决于它们从中被调用的点。通常,它们会使用相同的参数反复调用。如果是这样,则记忆会产生巨大的加速因素。
如果我使用的是BLAS或LAPACK函数,则可能会发现例程中花费了大量时间来复制数组,乘以矩阵,choleski变换等。
复制数组的例程不是为了提高速度而是为了方便。您可能会发现有一种不太方便但更快的方法。
用来对矩阵进行乘法或求逆或进行choleski变换的例程往往具有指定选项的字符参数,例如上三角或下三角的“ U”或“ L”。同样,这些是为了方便。我发现,由于我的矩阵不是很大,因此例程花费了一半以上的时间来调用子例程,以比较字符,从而破译选项。编写最昂贵的数学例程的专用版本可以大大提高速度。
如果我可以扩展后者:矩阵乘法例程DGEMM调用LSAME来解码其字符参数。查看被认为是“良好”的剖析时间(唯一值得关注的统计数据)可能会显示DGEMM使用总时间的某些百分比(例如80%)和LSAME使用总时间的某些百分比(例如50%)。看看前者,您可能会想说“必须对其进行大量优化,所以对此我无能为力”。看着后者,您会很想说:“嗯?这是怎么回事?那只是个小小的例行程序。此探查器一定是错误的!”
没错,只是没有告诉您您需要知道什么。随机暂停显示的是DGEMM在堆栈样本中占80%,而LSAME在堆栈样本中占50%。(您不需要太多示例来检测到这一点。通常10个示例就足够了。)此外,在许多示例中,DGEMM正在从几行不同的代码行调用LSAME。
所以现在您知道了为什么这两个例程都花那么多的包容时间。你也知道在你的代码,他们正在从所谓的花这么长的时间。这就是为什么我使用随机暂停并剖析探查器的黄疸病的原因,无论它们的制作水平如何。他们对获取测量值比告诉您发生的事情更感兴趣。
可以很容易地假设数学库例程已被优化到n级,但实际上它们已经被优化以可用于多种用途。您需要查看实际发生的情况,而不是容易理解的情况。
添加:因此,请回答您的最后两个问题:
首先要尝试的最重要的事情是什么?
拿10-20个堆栈样本,不要只是汇总它们,了解每个样本在告诉您什么。首先,最后和之间进行此操作。(年轻的天行者没有“尝试”。)
我怎么知道我可以获得多少性能?
1 /(1 − x )n = 10为0.5,加速比为2。这是分布:

如果您不愿冒险,则是的,小于0.1的可能性很小(.03%),对于加速比小于11% 。但是,对于大于10的加速比,等于0.9的概率是相等的!如果您按程序速度成正比地赚钱,那可不是什么好事。
正如我之前向您指出的,您可以重复执行整个过程,直到不再执行为止,并且复合加速比可能会很大。
添加:针对Pedro对误报的关注,让我尝试构建一个可能会出现误报的示例。除非我们两次或多次看到潜在的问题,否则我们不会采取任何行动,因此,我们期望在尽可能短的时间内看到问题,尤其是在样本总数很大时,会出现误报。假设我们抽取了20个样本,并且看到了两次。估计它的成本是总执行时间(即分发方式)的10%。(分布的平均值较高-为。)下图中的下部曲线是其分布:

考虑一下我们是否抽取了多达40个样本(比我一次采样的样本还要多),却只发现其中两个样本有问题。如较高的曲线所示,该问题的估计成本(模式)为5%。
什么是“误报”?如果您解决了一个问题,却发现收益比预期的要小,您为解决该问题感到后悔。曲线显示(如果问题是“小”的话),尽管增益可能小于显示它的样本比例,但平均而言它将更大。
风险要严重得多-“假阴性”。就是说有问题,但是没有找到。(造成这种情况的是“确认偏见”,在这种情况下,缺乏证据往往会被视为缺乏证据。)
使用事件探查器(一个不错的工具)得到的结果是,您可以得到更精确的度量(因此,误报的可能性会降低),而对于问题实际所在的信息的精确度却要低得多(因此,发现并获得问题的机会就会减少)任何收益)。这限制了可以实现的整体速度。
我鼓励分析器用户报告他们实际在实践中获得的加速因素。
还有另一点要提到。佩德罗关于误报的问题。
他提到在高度优化的代码中发现小问题时可能会遇到困难。(对我来说,一个小问题是占总时间的5%或更少。)
由于完全有可能构建一个除5%之外完全最佳的程序,因此只能凭经验解决此问题,如此答案所示。从经验中得出的结论是这样的:
如所写,程序通常包含多个优化机会。(我们可以称它们为“问题”,但它们通常是非常好的代码,仅能进行相当大的改进。)此图说明了一个人工程序,该程序花费一些时间(例如100s),其中包含问题A,B,C, ...发现并修复后,可节省原始100s的30%,21%等。

请注意,问题F花费了原始时间的5%,因此它“很小”,并且在没有40个或更多样本的情况下很难找到。
但是,前10个样本很容易发现问题A。**修复该问题后,程序仅花费70秒钟,加速比为100/70 = 1.43x。这不仅使程序更快,而且还以该比例放大了其余问题所占的百分比。例如,问题B最初花费21s,占总数的21%,但是在删除问题A之后,B花费了70s中的21s,即30%,因此在重复整个过程时更容易发现。
一旦重复执行该过程五次,现在执行时间为16.8s,其中问题F为30%,而不是5%,因此10个样本很容易找到它。
这就是重点。根据经验,程序包含一系列具有大小分布的问题,找到并解决的任何问题使查找剩余的问题变得更加容易。为了做到这一点,没有一个问题可以跳过,因为如果有的话,它们就坐在那儿花时间,限制了总的加速,并且无法放大剩余的问题。 因此,找到隐藏的问题非常重要。
如果问题A到F被发现并解决,则加速比为100 / 11.8 = 8.5倍。如果错过了其中之一,例如D,则加速仅为100 /(11.8 + 10.3)= 4.5倍。 这就是假阴性所付出的代价。
因此,当探查器说“这里似乎没有什么大问题”(即,好的编码器,这实际上是最佳代码)时,也许是正确的,也许不是。(错误的否定。)您不确定是否要解决更多问题以提高速度,除非您尝试其他分析方法并发现有问题。根据我的经验,概要分析方法不需要摘要中的大量样本,而需要少量样本,在这些样本中,对每个样本的了解都足够透彻,可以识别出进行优化的任何机会。
**至少需要2次命中才能找到问题,除非一个人事先知道有一个(近)无限循环。(红色刻度表示10个随机样本);当问题为30%时,获得2个或更多匹配的平均样本数为(负二项分布)。10个样本以85%的概率找到它,20个样本-99.2%(二项式分布)。要获得发现问题的可能性,请在R中进行评估,例如:。1 - pbinom(1, numberOfSamples, sizeOfProblem)1 - pbinom(1, 20, 0.3) = 0.9923627
增加:节省的时间遵循Beta分布,其中是样本数,是显示问题的数。但是,加速比等于(假设所有均被保存),了解的分布将很有趣。事实证明,遵循BetaPrime分布。我用200万个样本对其进行了模拟,得出了以下行为:
distribution of speedup
ratio y
s, n 5%-ile 95%-ile mean
2, 2 1.58 59.30 32.36
2, 3 1.33 10.25 4.00
2, 4 1.23 5.28 2.50
2, 5 1.18 3.69 2.00
2,10 1.09 1.89 1.37
2,20 1.04 1.37 1.17
2,40 1.02 1.17 1.08
3, 3 1.90 78.34 42.94
3, 4 1.52 13.10 5.00
3, 5 1.37 6.53 3.00
3,10 1.16 2.29 1.57
3,20 1.07 1.49 1.24
3,40 1.04 1.22 1.11
4, 4 2.22 98.02 52.36
4, 5 1.72 15.95 6.00
4,10 1.25 2.86 1.83
4,20 1.11 1.62 1.31
4,40 1.05 1.26 1.14
5, 5 2.54 117.27 64.29
5,10 1.37 3.69 2.20
5,20 1.15 1.78 1.40
5,40 1.07 1.31 1.17
前两列给出了加速比的90%置信区间。除了的情况以外,平均加速比等于。在那种情况下,它是不确定的,实际上,随着我增加模拟值的数量,经验均值也会增加。小号= Ñ ÿ
这是5个,4个,3个和2个样本中2个匹配项的加速因子及其均值分布的图。例如,如果抽取了3个样本,并且其中有2个碰到一个问题,并且可以消除该问题,则平均加速因子将是4倍。如果仅在2个样本中看到2个命中,则平均加速速度是不确定的-从概念上讲,因为无限循环的程序以非零概率存在!

您不仅必须具有对编译器的全面了解,而且还具有对目标体系结构和操作系统的全面了解。
如果您想压缩每一刻的性能,那么每次更改目标体系结构时,都必须进行调整并重新优化代码。在一个CPU的下一个修订版中,使用一个CPU进行的优化可能会变得次优。
一个很好的例子就是CPU缓存。从CPU具有快速小的高速缓存将你的程序一个一个稍微慢一些,稍微更大的缓存和你的分析可能显著改变。
即使目标体系结构不变,对操作系统的低级更改也会影响性能。Spectre和Meltdown缓解补丁对某些工作负载产生了巨大影响,因此,这些补丁可能会迫使您对优化进行重新评估。
在开发高度优化的代码时,您需要保持模块模块化,并轻松地交换同一算法的不同版本,甚至可能根据可用资源和资源的大小/复杂程度选择运行时使用的特定版本。要处理的数据。
模块化还意味着能够使用相同的测试套件上所有的优化和未优化的版本,让您确认它们都具有相同的行为,并在快速简介每一个像对等的比较。我将在“ 如何记录和教其他人“优化超出识别范围”的计算密集型代码?。
此外,我强烈建议您看一看Ulrich Drepper的出色论文,《每个程序员应该了解的内存》,其标题是对David Goldberg同样出色的《每个计算机科学家应该知道的浮点算术》的致敬。
请记住,每次优化都有可能成为将来的反优化方法,因此应将其视为可能的代码味道,并将其降至最低。我的回答是:编码时微优化是否重要? 从个人经验中提供了一个具体的例子。
我认为您对这个问题的措辞过于狭narrow。我认为,一种有用的态度是假设只有对数据结构和算法进行更改才能对超过几百行的代码产生显着的性能提升,我相信我还没有找到一个反例来这个主张。
您应该做的第一件事就是分析您的代码。您想在开始优化之前找出程序的哪些部分在拖慢您的速度,否则您可能最终对代码的一部分进行了优化,但它们并没有占用太多的执行时间。
的Linux
gprof相当不错,但是它只能告诉您每个函数而不是每一行要花费多少时间。
苹果OS X
您可能想尝试Shark。可在Apple Developer站点的下载>开发人员工具> CHUD 4.6.2(此处为旧版本)下找到。CHUD还包含其他概要分析工具,例如BigTop前端,PMC索引搜索工具,Saturn函数级概要分析器和许多其他命令。Shark将带有命令行版本。
优化代码必须谨慎进行。我们还假设您已经调试了代码。如果遵循某些优先级,则可以节省大量时间,即:
尽可能使用高度优化(或专业优化)的库。一些示例可能包括FFTW,OpenBlas,Intel MKL,NAG库等。除非您很有才华(例如GotoBLAS的开发人员),否则您可能无法击败专业人士。
使用探查器(此线程中已经列出了以下列表中的几个- Intel Tune,valgrind,gprof,gcov等)来找出代码中花费最多时间的部分。没有必要浪费时间来优化很少调用的代码部分。
从探查器结果中,查看代码中花费时间最多的部分。确定算法的本质是CPU限制还是内存限制?每个都需要一套不同的优化技术。如果您遇到大量的高速缓存未命中,则内存可能是瓶颈-CPU浪费了时钟周期,等待内存可用。考虑一下循环是否适合您系统的L1 / L2 / L3缓存。如果您的循环中有“ if”语句,请检查分析器是否对分支预测错误有任何说法?您的系统的分支错误预测惩罚是什么?顺便说一句,您可以从《英特尔优化参考手册》 [1]中获取分支错误预测数据。请注意,分支错误预测的惩罚是特定于处理器的,如您将在英特尔手册中看到的。
最后,解决探查器确定的问题。这里已经讨论了许多技术。还提供了许多有关优化的良好,可靠,全面的资源。仅举两个例子,有《英特尔优化参考手册》 [1]和由阿格纳·福格撰写的五本优化手册[2]。请注意,如果编译器已经做过某些事情,则可能不需要做这些事情,例如,循环展开,对齐内存等。请仔细阅读编译器文档。
参考文献:
[1]英特尔64和IA-32架构优化参考手册:http : //www.intel.sg/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf
[2] Agner Fog,“软件优化资源”:http ://www.agner.org/optimize/
这可能更像是元答案而不是答案...
您必须非常熟悉编译器。您可以通过阅读手册并尝试使用这些选项来最有效地获取此信息。
@Pedro分发的许多好建议可以通过调整编译而不是程序来实现。
对程序进行概要分析的一种简单方法(在Linux中)是perf在stat模式下使用。最简单的方法就是像这样运行
perf stat ./my_program args ...
它将为您提供许多有用的性能统计信息:
Performance counter stats for './simd_test1':
3884.559489 task-clock # 1.000 CPUs utilized
18 context-switches # 0.005 K/sec
0 cpu-migrations # 0.000 K/sec
383 page-faults # 0.099 K/sec
10,911,904,779 cycles # 2.809 GHz
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
14,346,983,161 instructions # 1.31 insns per cycle
2,143,017,630 branches # 551.676 M/sec
28,892 branch-misses # 0.00% of all branches
3.885986246 seconds time elapsed
有时它还会列出D缓存的加载和未命中。如果您看到很多缓存未命中,则说明您的程序占用大量内存,并且不能很好地处理缓存。如今,CPU的速度快于内存带宽,通常问题始终是内存访问。
您也可以尝试使用perf record ./my_program; perf report哪种简便的方法进行分析。阅读手册页以了解更多信息。