BOLT重新布局函数和基本块的目标是优化指令缓存(ICache)的性能。通过确保经常一起执行的代码块在物理存储中是紧密关联的,可以降低ICache不命中的概率。以下是BOLT重新布局函数和基本块的方法:
-
收集profile信息:
- 运行程序并使用工具(如Linux的
perf
工具)收集性能数据。这些数据会告诉BOLT哪些函数和基本块被最频繁地执行,以及它们之间的调用关系。
- 运行程序并使用工具(如Linux的
-
函数重新布局:
- BOLT会根据函数的调用频率来重新布局它们。最常被调用的函数被放置在一起,这样它们在ICache中可能是相邻的或接近的。
- 此外,BOLT考虑到跳转的距离,因为长距离跳转在某些架构中可能会产生额外的开销。因此,经常一起调用的函数会尽量放在接近的位置。
-
基本块重新布局:
- 在函数内部,BOLT重新布局基本块[1],优化那些“热”路径或经常执行的路径。
- 这种布局方法确保了热路径中的基本块在物理存储中是连续的或相邻的,从而优化了ICache的命中率。
- BOLT还会尝试减少不必要的跳转,例如,它可以直接将两个经常连续执行的块放在一起,消除中间的跳转。
-
考虑页面效果:
- 为了进一步优化性能,BOLT还考虑了页面的效果。例如,它会尽量确保经常一起执行的代码块位于同一个内存页面上,以减少页面错误和其他相关的缓存效果。
-
循环处理:
- 循环是计算密集型应用程序中的关键组件,因此BOLT为循环提供了特殊处理。它会确保循环体中的代码紧凑并且优化,以达到最佳的ICache性能。
-
克服二进制大小增长:
- 重新布局可能会增加二进制的大小,因为某些跳转变得更远,需要更多的指令。BOLT采用了策略,如跳转表[2],来克服这一问题。
-
考虑其他缓存和硬件效果:
- BOLT不仅仅关注ICache。它还尝试优化其他缓存和硬件效果,如分支预测、数据缓存和前端抖动。
通过上述方法,BOLT实现了在真实工作负载下优化应用程序性能的目标。而且,由于它在编译后阶段工作,所以它可以与其他编译器优化相结合,进一步提高性能。
[1] BOLT在重新布局基本块和函数时可能会引起某些跳转的目标移动到更远的位置,这在某些情况下可能会导致跳转指令的长度增加,因此增加了整体二进制的大小。为了克服这一挑战,BOLT使用了一些策略,其中包括跳转表。
以下是BOLT如何使用跳转表和其他策略来克服由于重新布局引起的二进制大小增加的:
-
跳转表(Indirect Jump Tables):
- 当直接跳转的距离超过某个阈值时,BOLT可能会选择使用间接跳转替代直接跳转。间接跳转通过跳转表进行,它使用较短的指令序列来查找真正的跳转目标。
- 在跳转表中,每个目标都有一个唯一的偏移量。执行代码会查找表中的这个偏移量,然后使用它来跳转到正确的目标。
- 这样,即使目标移动到更远的位置,跳转指令的大小也不会增加,因为它总是引用固定大小的表。
-
跳转延迟槽优化:
- 在某些架构中,跳转指令后面可能有一个“延迟槽”,该指令会在跳转发生后执行。BOLT尝试找到有意义的指令放入这个槽中,从而提高性能而不增加代码大小。
-
共享跳转目标:
- 在某些情况下,两个或多个跳转可能有相同的目标。通过确保这些跳转共享相同的目标地址,BOLT可以避免为每个跳转单独增加指令。
-
密集区域的保护:
- BOLT会特别注意那些代码密集的区域,确保在这些区域内的跳转不会因重新布局而变得过长。
-
调整阈值和参数:
- BOLT允许用户调整某些参数,以平衡二进制大小和性能。例如,用户可以设置跳转距离的阈值,超过该值的跳转将使用跳转表。
通过上述策略,BOLT能够在提高性能的同时,尽量避免因重新布局导致的二进制大小增加。
[2] 基本块是编译器和程序分析领域中的一个核心概念。它代表了程序中连续执行的、没有分支的一系列指令。也就是说,在进入基本块的第一条指令后,后续的指令将会顺序执行,不会有任何跳转,直到到达基本块的末尾。同样,基本块只有一个入口点和一个出口点。
详细解释如下:
-
入口和出口:
- 基本块有一个明确的开始点和结束点。开始点是块中的第一条指令,结束点是块中的最后一条指令。
- 从程序的任何其他位置,不能跳转到基本块的中间;同样地,一旦开始执行基本块的任何指令,将会连续执行块内的所有指令,直到结束。
-
无分支:
- 在基本块内部,没有分支或跳转指令,这意味着指令是连续执行的。
-
创建基本块的原因:
- 将程序划分为基本块有助于编译器进行分析和优化。因为基本块内部的指令顺序是固定的,所以可以在块内部进行各种局部优化。
- 基本块也是许多其他程序分析和优化技术的基础,如数据流分析和控制流分析。
-
示例:
1. int x = 10; 2. int y = 20; 3. if (x > y) { 4. x = x - y; 5. } else { 6. y = y - x; 7. } 8. print(x);
- 在上述代码中,可以识别出多个基本块:
- 1和2行构成一个基本块。
- 3行的条件检查是一个基本块。
- 4行是一个基本块。
- 6行是一个基本块。
- 8行是一个基本块。
- 在上述代码中,可以识别出多个基本块:
注意,由于if
和else
引入了分支,所以3行、4行和6行是不同的基本块。
当BOLT或其他编译器优化工具对程序进行优化时,它们会重点关注基本块,因为基本块提供了一个局部的、可以预测的上下文来进行各种优化,如重新布局、常量折叠、死代码消除等。在BOLT的上下文中,通过识别和优化那些经常执行(“热”)的基本块,可以显著提高程序在实际运行时的性能。