国外做测评的网站,大连承揽营销型网站公司,嘉兴门户网站,苏州吴江保洁公司接下来讨论如何使用CPU监控特性寻找CPU上运行的代码中可被调优的位置。
标准的算法和数据结构在性能敏感型工作负载并不总能表现的很好。例如#xff0c;在“扁平化”数据结构的冲击下#xff0c;链表基本上快被放弃了。传统链表中的每个节点都是动态分配的#xff0c;除了…接下来讨论如何使用CPU监控特性寻找CPU上运行的代码中可被调优的位置。
标准的算法和数据结构在性能敏感型工作负载并不总能表现的很好。例如在“扁平化”数据结构的冲击下链表基本上快被放弃了。传统链表中的每个节点都是动态分配的除了引入耗时的内存分配操作还可能让链表中所有元素分散在内存中导致随机内存访问。
二分搜索在排序数组中查找元素方面是最优的但是该算法经常会有很多分支预测错误的问题这就是为何线性搜索在小型少于20个元素整型数组上表现得最好。
本章尝试专注于CPU微架构相关的优化而不是覆盖所有你能想到的优化机会、不过也有必要列出上层的优化点 1. 使用开销更低的语言重写程序的性能关键部分 2. 分析程序中使用的算法和数据结构 3. 调优编译器参数检查至少使用了-O3与机器无关的优化功能、 -march启用针对特定CPU系列的优化功能和-flto启用过程间优化功能 4. 如果问题是高度并行化的计算考虑把程序线程化或者放到GPU上运行 5. 当等待IO操作时使用同步IO以避免阻塞 6. 利用更多的RAM来减少必须使用的CPU和IO量记忆、查找表、数据缓存、压缩等
数据驱动优化
数据驱动的优化是最重要的调优技术之一它基于对程序正在处理的数据的洞察聚焦于数据的分布及其在程序中的转换方式。典型的有SOA和AOS数据布局。如果程序遍历数据结构并且只访问指端b那么SOA会更好。然而如果程序遍历数据结构并且访问该对象的所有字段都需要进行许多操作那么AOS会更好。因为该数据的所有成员可能都会保留在相同的缓存行里。
另一个非常重要的数据驱动的优化是“小尺寸优化”其理念是提前为容器分配一定量的内存以避免动态内存分配。这对元素数据上限可以预测的中小尺寸容器非常有用。
实现的优化不一定对所有平台都有效果。例如循环阻塞非常依赖系统内存的层次特征尤其是L2和L3缓存大小。在程序将要运行的平台上测试这些变化是非常重要的。
CPU前端低效指后端在等待指令来执行但是前端不能给后端提供指令原因归类为2种缓存利用率和无法从内存中获取指令。建议只有当TMA显式较高的“前端bound”指标大于20%时才关注CPU前端的代码优化。
7.1 机器码布局
当编译器将源代码翻译为机器码时它会生成一个串行的字节列。其中指令在内存中放置的偏移位置也会反过来影响二进制文件的性能。
7.2 基本块
基本块是指只有一个入口和一个出口的指令序列。虽然基本块可以有多个前导和后继但是在基本块中间没有任何指令可以跳出基本块保证基本块中的每条代码只会被执行1次能大大地减少控制流图分析和转化的问题。
7.3 基本块布局
// hot path
if (cond)coldFunc();
// hot path again
如果cond通常为真那么就选默认布局。因为另一个布局通常做2次而不是1次跳转。但是coldFunc是一个错误处理函数并且不太可能会被经常执行选择保持热点代码间的直通并且把选取分支转化为未被选取分支。
选择热点代码间的直通的布局有原因如下 1. 未被选取的分支比被选取时耗时更少。一般情况下Intel CPU每个时钟可以执行2个未被选择的分支但是每2个时钟周期才能执行一个被选取的分支。 2. 更充分利用指令和微操作缓存。因为所有热点代码都是连续的所以没有缓存行碎片化问题。 3. 被选取的分支对于读取单元来说也更耗时。每个被选取的跳转指令都意味着跳转之后的字节都是都无效的。
可以使用__builtin_expect(cond, 0)注解告诉编译器概率高低。
7.4 基本块对齐
性能会由于指令在内存中的偏移量而发生明显的变化。若循环跨越多条缓存行可能会导致CPU前端出现性能问题所以我们可以使用nop指令将循环指令向前移动以便让整个循环驻留在一条缓存行中。
LLVM使用-mllvm-align-all-blocks对齐基本块注意它们可能导致性能劣化插入nop指令会增加程序的开销尤其是当它们处于关键路径上。nop指令不需要被执行但是它们仍然需要从内存中读取、解码和退休额外地消耗前端数据结构和用于记账的缓冲区空间。
为了细粒度地控制对齐还可以使用ALIGN汇编指令针对实验场景开发人员先生成汇编列表然后插入ALIGN指令。
7.5 函数拆分
函数拆分的设想是把热点代码和冷代码区分开该优化对在热路径中具有复杂CFG和大量冷代码的函数是有益的。
void foo(bool cond1, bool cond2) {// hot pathif (cond1) {//large amount of cold code cond1}// hot pathif (cond2) {//large amount of cold code cond2}
}// 优化后
void foo(bool cond1, bool cond2) {// hot pathif (cond1) {cold1()}// hot pathif (cond2) {cold2()}
}void cold1() __attribute_((noinline)) { // cold code (1)};
void cold2() __attribute_((noinline)) { // cold code (2)};
图中我们只保存了热路径的call指令所以下一个热点代码指令可能会驻留在相同的缓存行提升CPU前端数据结构指令缓存和DSB的利用率。留意其中的另一个重要思想禁止内联冷函数。最后创建的新函数要放在.text段之外。如果从不调用该函数俺么它不会在运行时加载到内存中所以可能会改善内存占用情况。
7.6 函数分组
热点函数可以被分组在一起以进一步提升CPU前端缓存的利用率减少需要读取缓存行的数量。
链接器负责程序在最终的二进制输出中所有函数的排列布局。LLVM的LLD链接器使用--symbol-ordering-file优化函数的布局。
HFSort工具基于剖析数据自动生成分区排序文件。
7.7 基于剖析文件的编译优化
大多数编译器都有一组转换功能可以根据反馈给它们的剖析数据来调整算法被称为基于剖析文件的编译优化Profile Directed OptimizationPGO。
剖析数据生成方式有二代码插桩核基于采样的剖析。 1. 先利用LLVM编译器使用-fprofile-instr-generate告诉编译器生成插桩代码。然后LLVM编译器使用-fprofile-inst-use利用剖析数据重新编译程序并生成PGO调优的二进制文件。 2. 基于采样生成编译器所需的剖析数据。然后AutoFDO把linux perf生成的采样数据转换为GCC和LLVM的编译器可以理解的形式。不过编译器会假设所有负载的表现都一样。
7.8 对ITLB的优化
内存地址中虚地址到物理地址的翻译是前端性能调优的另一个重要领域。通过把应用程序的性能关键代码部分地映射到大页上可以减少ITLB压力。这需要重新链接二进制文件在合适的页边界对齐代码段。除了使用大页用于优化指令缓存性能的标准技术也可以提升ITLB性能即重排函数让热点函数更集中通过LTO/IPO减小热点区域的大小使用PGO并避免过度内联。
7.9 总结
转换如何转换优点应用场景执行者基本块布局维护热点代码的直通未被选取的分支耗时更少缓存利用率更高任何代码尤其是由很多分支的代码编译器基本块对齐使用NOP指令对热点代码进行移位缓存利用率更高热点循环编译器函数拆分把冷代码拆分出来并放到单独的函数中缓存利用率更高当在热代码间存在大段冷代码的函数时具有复杂CFG的函数编译器函数分组把热点函数分组到一起缓存利用率更高有很多热点小函数链接器