编译器完成的配置文件引导优化是否会损害分析数据集未涵盖的案例?

sha*_*oth 5 language-agnostic optimization performance profiling compiler-optimization

这个问题不是特定于C++,AFAIK像Java RE这样的某些运行时可以动态地进行分析引导优化,我也对此感兴趣.

MSDN 像这样描述PGO:

  1. 我检测我的程序并在profiler下运行它
  2. 编译器使用分析器收集的数据自动重新组织分支和循环,以便减少分支错误预测,并且大多数情况下运行代码紧凑地放置以改善其位置

现在显然,分析结果将取决于使用的数据集.

通过正常的手动分析和优化,我会发现一些瓶颈并改善这些瓶颈,并可能保持所有其他代码不受影响.PGO似乎改进了经常运行的代码,但却使得很少运行代码变慢.

现在,如果经常在另一个程序将在现实世界中看到的数据集上运行那么慢的代码呢?与没有PGO编译的程序相比,程序性能是否会降低,退化的可能性有多大?换句话说,PGO是否真的提高了我的分析数据集的代码性能,并可能使其他数据集的性能恶化?有真实数据的真实例子吗?

pet*_*hen 9

免责声明:我没有做过更多关于PGO的事情而不是阅读它并尝试了一个示例项目以获得乐趣.以下很多是基于我的经验,即"非PGO"优化和有根据的猜测.TL; DR下面.

该页面列出了PGO完成的优化.让我们一个一个地看一下(按影响分组):

内联 - 例如,如果存在经常调用函数B的函数A,并且函数B相对较小,则配置文件引导的优化将在函数A中内联函数B.

寄存器分配 - 使用配置文件数据进行优化可以实现更好的寄存器分配

虚拟呼叫推测 - 如果虚拟呼叫或通过功能指针的其他呼叫经常以某个功能为目标,则配置文件引导的优化可以向有针对性的功能插入有条件执行的直接呼叫,并且可以内联直接呼叫.

这些显然改善了预测,无论一些优化是否得到回报.没有直接权衡非配置代码路径.


基本块优化 - 基本块优化允许在给定帧内暂时执行的通常执行的基本块放置在同一组页面(局部性)中.这最大限度地减少了使用的页数,从而最大限度地减少了内存开

功能布局 - 基于调用图和分析的调用者/被调用者行为,往往沿同一执行路径的函数放在同一部分中.

死代码分离 - 在分析期间未调用的代码将移动到附加到该组部分末尾的特殊部分.这有效地使这一部分远离常用页面.

EH代码分离 - 当特殊情况引导的优化可以确定异常仅在异常条件下发生时,异常执行的EH代码通常可以移动到单独的部分.

所有这些都可能减少非配置代码路径的位置.根据我的经验,如果此代码路径具有超过L1代码缓存的紧密循环(甚至可能使L2陷入困境),则影响将是显着或严重的.这听起来就像应该包含在PGO配置文件中的路径:)

死代码分离可以产生巨大的影响 - 两种方式 - 因为它可以减少磁盘访问.

如果您依赖快速的异常,那么您做错了.


大小/速度优化 - 程序花费大量时间的功能可以针对速度进行优化.

现在的经验法则是"默认优化大小,并且只在需要时优化速度(并验证它有帮助).原因是代码缓存 - 在大多数情况下,较小的代码也将是更快的代码,因为代码缓存.所以这种自动化你应该手动完成的工作.与全局速度优化相比,这只会在非常典型的情况下减慢非配置代码路径("奇怪的代码"或具有异常缓存行为的目标机器) .


条件分支优化 - 使用值探测器,概要文件引导的优化可以查找switch语句中的给定值是否比其他值更频繁地使用.然后可以从switch语句中提取该值.使用if/else指令可以完成相同的操作,优化器可以对if/else进行排序,以便if或else块首先放置,具体取决于哪个块更频繁为真.

除非你提供错误的PGO信息,否则我也会在"改进预测"下提交.

这可以支付很多费用的典型情况是运行时参数/范围验证以及在正常执行中永远不应该采用的类似路径.

破案将是:

if (x > 0) DoThis() else DoThat();
Run Code Online (Sandbox Code Playgroud)

在一个相关的紧密循环中,只分析x> 0的情况.


记忆内在 - 如果可以确定是否经常调用内在函数,则可以更好地确定内在函数的扩展.还可以基于移动或复制的块大小来优化内在函数.

同样,主要是更好的信息,很可能惩罚未经测试的数据.

示例: - 这是一个"有根据的猜测",但我认为这对整个主题来说非常具有说明性.

假设您memmove总是在长度为16字节的良好对齐的非重叠缓冲区上调用.

可能的优化是验证这些条件并在这种情况下使用内联MOV指令,memmove仅在不满足条件时调用一般(处理对齐,重叠和奇数长度).

在复制结构的紧密循环中,好处可能是显着的,因为您改善了局部性,减少了预期的路径指令,可能有更多的配对/重新排序机会.

然而,惩罚相对较小:在没有PGO的一般情况下,你要么总是调用完整的memmove,要么nline完整的memmove实现.优化添加一些指令(包括条件跳转)到相当复杂的东西,我假设最多10%的开销.在大多数情况下,由于缓存访问,这些10%将低于噪声.

但是,如果经常采用意外分支并且预期情况的附加指令与默认情况的指令一起推出L1代码缓存中的紧密循环,则会产生轻微的轻微影响.

请注意,您已经处于编译器可以为您做的限制之内.与代码高速缓存中的少量K相比,可以预期附加指令是几个字节.静态优化器可能会达到相同的命运,具体取决于它可以提升不变量的程度 - 以及你拥有多少.


结论:

  • 许多优化都是中立的.
  • 某些优化可能会对非配置文件的代码路径产生轻微的负面影响
  • 影响通常远小于可能的收益
  • 极少数情况下,其他有影响的病理因素可以强调小的影响
  • 很少有优化(即代码部分的布局)会产生很大的影响,但是可能的增益再次显着增加

我的直觉会进一步声称

  • 总的来说,静态优化器至少同样可能创建一个病态案例
  • 即使PGO输入不良,实际上也很难破坏性能.

在那个级别,我会比PGO优化失败更害怕PGO 实现错误/缺点.


kro*_*lth 1

PGO 肯定会影响运行频率较低的代码的运行时间。毕竟,您正在修改某些函数/块的局部性,这将使现在在一起的块更加缓存友好。

我所看到的是,团队确定了他们的高优先级场景。然后他们运行这些来训练优化分析器并衡量改进情况。您不想运行 PGO 下的所有场景,因为如果这样做,您还不如不运行任何场景。

正如与性能相关的所有事情一样,您需要在应用之前进行衡量。检查您最常见的场景,看看它们是否通过使用 PGO 培训得到了改善。并且还要衡量不太常见的情况,看看它们是否有回归。