一个JIT编译器能做到什么,而AOT编译器做不到?

6
一种即时编译器(JIT)可以根据预先不可用的运行时信息对程序进行优化,而静态编译器(AOT)则不能。
这种运行时信息最明显的例子是目标平台,例如:程序正在运行的确切CPU或任何加速器,例如可能可用的GPU。这就是OpenCL被JIT编译的意义所在。
但是假设我们预先知道目标平台是什么:我们知道哪些SIMD扩展将可用等。那么,JIT编译器可以利用哪些其他运行时信息,这些信息对于AOT编译器不可用呢?
类似HotSpot的JIT编译器将自动优化程序的热点... 但是一个AOT编译器不仅可以优化整个程序,还可以优化所有热点,是吗?
我想要一些具体示例,说明JIT编译器可以执行的特定优化,而AOT编译器无法执行。如果您能提供任何关于这种优化在“现实世界”情况下有效性的证据,那么会有额外加分。

2
你读过这个吗:https://dev59.com/mXI95IYBdhLWcg3w5SOh - Anubhav Srivastava
@AnubhavSrivastava 感谢您提供的链接。这是一个类似的问题,但是最受欢迎的答案和被接受的答案都没有回答我的问题。不过,在其他答案中有一些 JIT 优化的例子:跨库优化和使用跟踪树进行动态内联。我很想知道这些在实践中有多大的差异。 - c--
反射是一个臭名昭著的问题,无法仅通过字符串静态确定所需的类型。 - Hans Passant
3个回答

6

JIT可以基于运行时信息进行优化,这导致了在编译时无法证明的更严格的边界条件。例如:

  • 它可以看到一个内存位置没有别名(因为采取的代码路径从未给它命名),因此将变量保留在寄存器中;
  • 它可以消除永远不可能发生的条件测试(例如,基于当前参数值);
  • 它可以访问完整的程序,并在适当的位置内联代码;
  • 它可以根据运行时的具体使用模式执行分支预测,使其最优。

内联原则上也适用于现代编译器/链接器的链接时优化,但如果在整个代码中应用,可能会导致禁止性代码膨胀;在运行时,它只能在必要的地方应用。

如果程序被编译两次并在其中进行测试运行,则可以通过正常编译器来改进分支预测;在第一次运行中,代码被插装以生成分析数据,在生产编译运行中使用该数据来优化分支预测。如果测试运行不典型,则预测也不够理想(而且通常很难产生典型测试数据,或者使用模式可能会随着程序的生命周期而变化)。

此外,静态编译的链接时间和运行时数据优化需要在构建过程中进行大量努力(在我生命中工作过的10个左右的地方中,我没有看到它们在生产中使用);使用JIT默认开启它们。


谢谢,这正是我想要的东西。 - c--

3
一个JIT编译器能做到什么,而AOT编译器不能呢?
理论上来说,没有什么区别,因为如果愿意的话,AOT编译器可以在生成的代码中插入JIT编译器(或者生成自修改代码,生成123个备选版本并根据运行时信息选择使用哪个版本等)。
但实际上,AOT编译器受到编译器设计者处理复杂性的限制、编译语言以及编译器的使用方式等方面的影响。例如,一些编译器(如英特尔的ICC)会生成多个版本的代码,并且(在运行时)根据运行的CPU决定使用哪个版本,但大多数编译器并没有被设计成这样做;许多语言没有提供任何控制“局部性”(减少TLB和缓存未命中的机会)的方法;通常编译器的使用方式会创建障碍,防止优化(例如,分离的“编译单元”/目标文件稍后链接在一起,可能包括动态链接,在这种情况下,AOT编译器无法进行整个程序的优化,只能对部分进行优化)。所有这些都是实现细节,而不是AOT的限制。
换句话说,在实践中,“AOT vs JIT”是实现的比较,而不是“AOT vs JIT”本身的真正比较。在实践中,AOT的性能很差,因为受到实现细节的影响,而JIT的性能略微差于差(因为JIT本身就不好,昂贵的优化根本不可行,因为它们是在运行时完成的)。唯一的原因是,JIT看起来“几乎和好的一样”,是因为它只是“几乎和坏的一样”。

1
如果AOT编译器将JIT编译器插入到生成的代码中,我认为我们可以说结果是JIT编译的。问题的重点是询问为什么会这样做 - JIT编译器能做什么,而AOT编译器不能自己完成呢?Peter A. Schneider在他的答案中给出了一些不仅仅是实现细节的例子。 - c--
@c--:如果AOT编译器实际上插入了JIT(对于某些程序部分认为它是有益的),那么它将与使用其他技巧获得相同好处的AOT编译代码几乎无法区分。换句话说,它仍然是AOT编译的,之后的一切都是在假设实现细节上吹毛求疵。对于Peter A. Schneider的回答,我可以找到一个单一的例子是正确的 - AOT编译器可以完成所有列出的事情(而无需将JIT插入AOT编译代码中),并且比使用JIT更有效率。 - Brendan
1
我很想知道AOT如何能够“消除一个基于当前参数的条件测试,其永远不可能发生...”,考虑到它并不知道“当前参数的值”。最好的AOT只能根据常量传播来消除无法到达的代码,对吗? - c--
@c--:我很想知道人们怎么会如此愚蠢,相信“测试/以确定(基于当前值)是否可以消除测试”有意义。他们认为你可以在没有额外的等同或更糟的测试的情况下决定是否可以消除测试吗?他们是否意识到现代CPU中的分支预测使消除测试成为一种毁灭性的性能损失(即使没有“测试/潜在避免测试”的愚蠢)? - Brendan
@c--:一个足够先进的AOT可以确定测试是否总是不必要的,但也可以确定更复杂的模式(简单示例;“只写一次”变量会导致测试从“最初总是失败”到“最终总是通过”,编译器可以在设置写入一次变量时修改函数指针,以便永远没有测试)。这些只取决于编译器的复杂性,而不是JIT支持者的妄想幻想,他们不够聪明,无法意识到JIT的大部分性能来自于库中的AOT编译的本机代码等。 - Brendan
1
没有必要称别人为愚蠢或者大喊“妄想幻想”。这个问题有两个部分:(a) JIT编译器能做到什么,而AOT编译器不能;(b) 有哪些证据表明(a)的答案在“实际应用”中确实有效。回答(a)是有效的,但也可以承认它们在实践中很少有帮助。 - c--

0

一个优点是JIT编译器可以持续地分析代码并优化输出,例如对齐/不对齐一些代码块,取消优化某些函数,重新排列分支以减少预测错误…

当然,AoT编译器也可以执行基于配置文件的优化,但它们受到开发人员和测试人员所执行的测试用例的限制,这可能无法反映真实输入的动态性

例如,当Android在Kitkat中引入ART时,已经从仅使用AoT回归到混合方法的Nougat和后续版本,其中应用程序的某些部分会快速地进行提前编译,但较少优化,然后在运行配置文件结果时再次优化应用程序,同时手机正在充电

Android 7.0 Nougat在ART中引入了JIT编译器和代码分析,使其能够不断改进Android应用程序的性能。 JIT编译器与ART当前的Ahead of Time编译器相辅相成,并有助于提高运行时性能。

https://en.wikipedia.org/wiki/Android_Runtime

一些相关问题:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接