代码改变世界

内存性能工具:Part 4 改善分支预测

2010-07-02 17:22  Robbin  阅读(2169)  评论(1编辑  收藏  举报

7.4 改善分支预测

在6.2.2节中,曾经提到可以通过分支预测和禁止重排序来改善L1i的效率:通过_builtin_expect的静态预测和基于剖析引导的优化(PGO)。正确的分支预测可以改善性能,不过 这里我们只关系内存使用情况的改善。

*译注:PGO(profile-guided optimization)是一种编译器优化技巧,不同于传统分析源代码的优化方式,通过不同测试收集到采样信息来进行优化,而不是传统的启发式方法进行优化,VC++,Intel C++ Compiler和GCC都实现了这个功能,可以看看这里,http://blogs.msdn.com/b/vcblog/archive/2008/11/12/pogo.aspx

使用__builtin_expect(使用likely和unlikely宏更好些)很简单。定义放在了一个内部的头文件中,编译器负责处理其它事项。但有一个小问题:程序员很容易在使用likely时而实际应该使用unlikely,相反的情况也存在。即使大家使用像oprofile这样的工具来检查错误的分支预测和缓存未命中,这些错误也是很难发现的。

但还是有一个简单的方法。在9.2节的代码中展示了一种likely和unlikely的定义,会在运行时主动的测量静态预测是错误还是正确。然后程序员或测试人员可以检查结果并进行可能的调整。测量结果并不反映程序的性能,它只是对程序员的静态预测进行了测试。更多的细节,可以从上面所说的章节中找到,就在代码附近。

现在gcc中的PGO是非常简单易用的。它是一个3阶段流程,当然要先满足一些条件才行。首先,所有代码必须在编译时加入选项-fprofile-generate。这个选项需要传递到所有文件,并在链接程序时也要加上。将打开选项和没打开选项的obj文件混合在一起是可以的,但PGO对没有打开选项的文件是没有什么用处的。

编译器生成的二进制文件除了稍微大了一点和慢了一点外一切如常,这是因为它会记录(也会输出)所有关于分支进入或未进入的信息。编译器也可以为每个输入文件产生一个.gcno文件。这个文件包含了代码中所有分支相关的信息。可以对它更进一步的处理。

一旦程序的二进制执行文件可用,它应该以不同程度的负载执行。不管使用哪种负载,最终的二进制都会被进行优化以执行这个任务。可以连续执行程序,也很必要;所有的执行结果都会写到同一个文件。程序终止之前,所有在程序运行时收集到的信息会写到gcda文件中。这些文件会被放在和源文件相同的目录下。程序可以在任一目录下执行,也可以拷贝二进制文件到其它地方,但源文件所在目录必须可读和可写。另外,每个源文件都有对应的输出文件。如果程序执行了多次,那么之前执行时生成的gcda文件要放在源文件目录下否则无法将执行数据合并到同一个文件中去。

当一组典型的测试执行之后,则需要重新编译程序。编译器可以找到源文件目录下的gcda文件。这些文件不能移走,因为编译器无法找到它们并且内部的checksum值也就不再匹配了。重编译时,使用-fprofile-use来替换-fprofile-generate选项。不要进行任何可能导致生成代码改变的源码级修改是非常必要的。也就是说可以增删空格或编辑注释,但不要增加新的分支或其它基本结构导致之前收集到数据失效,并且编译也会失败。

这就是程序员需要做的,真的是相当简单。最重要的是挑选不同程度的负载进行测量。如果测试的负载不和程序实际运行的程度相匹配,那么执行的优化实际会非常糟糕。因此,PGO很难有效用于库函数。库函数可能用于非常多的场景中,并且差异很大。除非测试例确实很简单,否则使用静态分支预测__builtin_expect通常效果更好。

简单说一下.gcno和.gcda文件。它们是二进制文件故而无法直接的用于分析。但是可以使用gcov工具来查看它们,它也是gcc工具包的一部分。这个工具主要用于覆盖率分析(其得名所在)不过和PGO使用相同的文件格式。gcov工具为包含可执行代码的文件(可能包含系统头文件)生成以gcda为扩展名的输出文件。这些文件列举了带标注的代码,根据传递给gcov的参数,可以包含分支计数,可能性,等等。