热点函数可以被分组到一起以进一步提升CPU前端缓存的利用率,当热点函数被分组在一起时候,他们可能会共用相同的缓存行,这会减少CPU需要读取的缓存行数量。
图44给出了被分组函数foo,bar和zoo的图形化展示。默认布局需要读取四个缓存行,而在优化布局中,函数foo,bar和zoo的代码只需要三个缓存行。此外,当我们从foo调用zoo时,zoo的开始部分已经在指令缓存中了,因为我们读取过该缓存行。
与前面的优化类似,函数分组提升了指令缓存和DSB缓存和利用率。当有很多小热点函数时,该优化表现最好。
连接器负责程序在最终二进制输出中所有函数的排列布局。虽然开发者可以尝试自己重排程序的函数,但是不能保证产生期望的物理布局。几十年来,人们一直使用连接器脚本来完成这项工作,如果你使用的是GNU连接器,那么你还需要使用这种方法,Gold连接器 有更简单的方法可以做这件事。要使用Gold连接器生成期望的函数顺序,可以先用-ffunction-sections 选项来编译代码,从而把每个函数放到单独的分区,然后,使用--section-ordering-file=order.txt 选项编译,该选项可以输入一个包含能反应最终期望布局的函数排序列表的文件。LLD连接器是LLVM编译器基础设施的一部分,也有相同的功能,可以通过--symbol-ordering-file选项使用。
把调用函数放一起,子函数放一起。
另一种解决热点函数分组问题的方法是HFSort工具实现,该工具可以基于剖析数据自动生成分区排序文件。通过该工具,工程师在类似Facebook,百度和维基百科这样的大型分布式云应用上实现了2% 的性能提升。最近HFSort集成到了FaceBook 的HHVM,不再作为单独的工具,LLD连接器采用了HFSort算法的实现,根据剖析数据对分区进行排序。
7.7 基于剖析文件的编译优化
编译程序和乘胜最优汇编代码都是启发式的行为。在特定的场景,为了达到最优性能,代码转码算法有很多多边角场景。因为编译器需要做很多决策,所以需要基于某些典型的场景猜测出最好的选择。例如,当决定某个函数是否需要被內联时,编译器需要考虑该函数被调用的次数,但是问题是编译器并不能提前知道这些信息。
如果剖析信息方便获得的话,基于指定剖析信息编译器可以做出更好的优化决策,大多数编译器中都有一组转换功能,可以根据反馈给他们的剖析数据来调整算法,这组转换功能被称为基于剖析文件的编译优化,Profile Guided Optimization, PGO。 在文献中,有时可以发现反馈定向优化,FDO。这个术语,本质上它与PGO指的是同一个概念。通常,有剖析数据时编译器会依赖剖析数据,没有剖析数据时编译器会依赖剖析数据,没有剖析数据时,将使用其启发式标准算法。
使用PGO让真实的负载性能优化15%时很常见的,PGO不仅可以提升內联功能和代码布局,还会优化寄存器分配等。
剖析数据可以通过两种方法生成,代码插桩和基于采样的剖析。两者的用法都相对简单,并且在5.8节我们也讨论过它们相应的优缺点。
第一种方法需要先利用LLVM编译器使用-fprofile-instr-generate 选项编译器程序,这样会告诉编译器生成插桩代码,这些插桩会在运行时采集剖析信息。然后,LLVM编译器使用-fprofile-instr-use选项利用剖析数据重新编译器程序,并生成PGO调优的二进制文件,使用Clang 的PGO指导,请参考LLVM文档。GCC编译器使用了不同的编译器选项,-fprofile-generate 和-fprofile-use。具体请参考GCC文档。
第二种方法基于此阿阳生成编译器所需要的剖析数据,然后利用AutoFDO工具吧Linux perf生成的采样数据转为类似GCC和LLVM的编译器可以理解的格式
编译器会盲目的使用你提供的剖析数据。编译器会假设所有负载的表现都一样,只会针对单一的负载优化应用程序,PGO用户需要非常小心选取需要剖析负载,因为当优化应场景时,另一个场景可能会被劣化。幸运的事,由于不同负载的剖析数据可以合并在一起代表应用程序的一组使用场景,所以不一定只是一个负载场景。
7.8 对ITLB的优化
内存地址中的虚拟地址到无力地址翻译时调优前端性能的另一个重要领域,这些翻译主要由TLB完成,TLB在某些条目缓存了最近使用过的内存页面翻译地址。当TLB不能完成翻译请求时,需要进行耗时的内核页表遍历,一计算每个引用的虚拟地址的正确物理地址当TMA显示高ITLB开销时,下面提供的建议可能有所帮助。
通过吧应用程序性能关键代码部分映射到大页上,可以减少ITLB的压力。需要这需要重新链接二进制文件,子啊合适的页边界对齐代码段,以准备大页映射。除了使用大页,用于优化指令缓存性能的标准技术也可以用于提升ITLB性能,即重拍函数让热点函数更集中,通过LTO/IPO减小热点区域的大小,使用PGO并避免过度內联。
转换 |
如何转换 |
为何有益 |
什么场景 |
执行者 |
|
基本块布局 |
维护热点代码直通 |
避免分支耗时,缓存利用率高 |
分之多的代码 |
编译器 |
|
基本块对齐 |
使用NOP指令对热点代码进行移位 |
缓存利用率高 |
热点循环 |
编译器 |
|
函数拆分 |
把冷代码拆放到独立的函数中 |
缓存利用率更高 |
编译器 |
||
函数分组 |
热点函数分组到一起 |
缓存利用率更高 |
有很多热点小函数 |
链接器 |