这个问题是用来跟踪我们正在考虑的PGO优化机会列表。当我们开始处理其中的任何一个时,它应该被分成自己的问题。我们会随着时间的推移编辑和添加到这个列表中。
- Inlining
- 间接调用去虚拟化:如果分析结果显示某个特定的闭包或方法是间接调用的最常见目标,那么可以针对调用者进行特殊处理,检查是否为目标,如果是,则直接调用(甚至可能内联目标)。完成一些后续工作:
- cmd/compile: PGO devirtualization of closures #64675
- cmd/compile: include PGO devirtualization targets in export data #64676
- 冷路径上的动态逃逸:如果分析结果显示唯一的原因是为了在冷路径上发生逃逸,而热路径上没有分配逃逸,那么我们可以乐观地栈分配并在冷路径上将分配动态移动到堆上。
- 模板化:根据分析结果中的类型信息对热泛型函数进行模板化。
- 局部基本块排序:对函数内的块进行排序,以便将热点块聚集在一起,并可能改善分支预测(尽管现代CPU上的后者优势很小)。
- 寄存器分配:寄存器分配目前使用启发式方法来确定热点路径并将溢出从该热点路径上移除。PGO可以告诉它真正的热点路径。
- 循环展开:与内联一样,循环展开是一种平衡二进制大小/i-缓存占用(同样,与内联一样,其主要优点在于现代CPU上的后续优化)。我们可以使用PGO将循环展开集中在热点循环上,这将在不影响二进制大小和编译时间的同时,最大限度地减少分析展开是否值得的开销。(参见cmd/compile: add unrolling stage for automatic loop unrolling #51302)
- 循环切换:在热函数中,循环切换更有可能值得代码大小增加。
- 架构特性检查切换和/或函数多版本化:对于热函数中的架构特性检查,提升这些特性检查,也许提升到函数级别。(这与循环切换密切相关,但不一定在循环中,而且我们事先知道提升条件是安全的。)
- 在分支和条件MOV之间选择:当条件可预测时,分支效率更高;当条件不可预测时,条件MOV效率更高。使用分析结果来选择这些。(可预测性很难从CPU分析结果中获得,因此这可能需要LBR分析结果;然而,LBR分析结果无法捕获条件MOVs,因此这可能只是不切实际的。)
- 函数排序:在二进制级别对函数进行聚类以提高局部性。
- 全局块排序:比函数排序更进一步。基本形式是热/冷分割,但可以比那更激进。
- Map/slice预填充:根据分配位置预填充Map和切片。(这需要比CPU分析结果更多的信息。)
- 按站点预填充栈:根据分析得到的栈大小分配初始栈。(这需要比CPU分析结果更多的信息。我们已经有了简单的运行时启发式方法,并且可能可以在运行时做得更好,而无需分析结果。)
- 生命周期分配:通过分配位置将分配与相似生命周期的分配进行聚类。(这也需要比CPU分析结果更多的信息. 这可能可以在运行时做到,但是复杂的技术也很昂贵。)
- 循环对齐:对小循环进行对齐可能会产生很大的性能影响,但也会增加二进制大小。有了PGO,我们只对热点循环进行这种操作。(例如,英特尔优化参考手册第3.4.1.4节)
(这个列表最初基于我以前的一个评论,这个问题部分是为了更好地跟踪和展示这个列表。)
任何基本块级别的优化很可能依赖于分析结果中的profile discriminators ( #59612 )。
9条答案
按热度按时间yftpprvb1#
对于这个"在分支和条件MOV之间进行选择",一些架构特性,如Arm的分支记录缓冲区扩展(BRBE),可能会有所帮助。顺便问一下,上述项目是谷歌正在考虑实施或已经实施的吗?还是它只是PGO可能感兴趣的优化机会列表,任何人都可以做?
but5z9lq2#
另一个可能性是自动将 Function Multi-Versioning 应用于具有大型循环的热点函数,这将极大地提高数学密集型工作负载的性能。
Intel's Clear Linux 使用这种方法来自动利用现代CPU指令 while still being backwards-compatible with x86-64-v1 。
从我的理解来看,Go没有特别成熟的自动向量化器,但有了 PGO-引导的 FMV 应该能在这个领域带来新的机会。
z9gpfhce3#
@erifan LBR支持已经在我们的计划中,我们一直在考虑这个问题。我不确定它是否属于这个问题(也许可以)。
j9per5c44#
对于这个"在分支和条件MOV之间选择"的问题,一些架构特性,如Arm的分支记录缓冲区扩展(BRBE),可能会有所帮助。
@erifan BRBE记录条件移动吗?那当然会很好。x86的LBR不会,这使得在x86上实现这个功能变得相当棘手。
顺便问一下,上述项目是谷歌正在考虑实施或已经实施的功能吗?还是只是PGO可能感兴趣的一系列优化机会?
这些都是我们正在考虑实施的功能,但我们并没有做出承诺。:)目前,我们肯定正在努力实现"间接调用去虚拟化"(1.21版本有一个这样的功能,但限制很大,我们希望在1.22版本中解除)。我们也认真考虑"冷路径上的动态逃逸",但我认为我们还没有开始实施它。我们在其他方面还没有取得进展。我相信Uber已经在"函数排序"方面做了一些工作,但我很久没有听到任何更新了。
drkbr07n5#
另一种可能性是自动将Function Multi-Versioning应用于具有大型循环的热点函数,这将极大地提高数学密集型工作负载的性能。
@myaaaaaaaaa ,谢谢。这就是我所说的“架构功能检查取消切换”,但我在我的列表中添加了“函数多版本化”这个词。函数多版本化是一种特定的方法,确实有一个很好的优点,如果你有一个从A到B的调用,并且A和B都是多版本化的,那么A可以直接调用B的正确版本。
nnt7mjpx6#
感谢@cherrymui
@aclements,我刚刚查看了BRBE文档,它也没有记录条件移动。
oxcyiej77#
还可能有机会扫描可以安全并行化的代码区域,并自动重写它们以作为单独的goroutine启动,通过通道发送它们的结果,从而有效地实现函数规模版本的instruction-level parallelism。
这通常会带来同步/上下文切换开销的风险,但PGO允许编译器仅将此优化应用于通常每次调用运行时间较长的函数/循环(例如,1-10ms)。
我不确定这种方法在实践中的适用性有多广泛。另一方面,我想想象成功检测到只有几个函数很容易创建足够的并行性来饱和甚至32线程的机器,因为goroutine计数会随着堆栈中每个并行化函数的增加而呈指数级增长。
5hcedyr08#
但是PGO允许编译器仅将此优化应用于通常每次调用运行时间较长的函数/循环(例如,1-10毫秒)。
我认为PGO(CPU分析)并不知道一个函数被调用的次数或者这些调用持续的时间有多长?
3pmvbmvn9#
所有预计算大小也可以动态地进行PGO。
这可以添加一个更专用的精确PGO。
我知道.NET添加了动态分析,人们对此感到很高兴。
动态地不逃逸也许也可以从那里受益,但我不确定。它可能会像分支被使用时一样逃逸,但也会在调用开始时逃逸,如果动态PGO认为这不应该被优化的话。
我们甚至可以使用OSR来进行所有其他优化,但那可能现在有点太多了。