JIT编译器与离线编译器的区别

43

JIT(即时编译器)是否比其他编译器,如C++更快的情况?

您认为在未来,JIT编译器是否只会进行一些小的优化和功能改进,但仍然保持类似性能,还是会出现突破,使其无限超越其他编译器?

多核范例似乎有一些前途,但并非万能。

任何见解?

11个回答

53

确实存在这样的情况。

  • JIT编译可以使用运行时分析来优化特定的情况,根据当前代码实际执行的特征进行测量,并根据需要重新编译“热点”代码。这不是理论上的问题;Java的HotSpot就是这样做的。
  • JIT编译器可以根据程序实际执行的硬件特性,对特定的CPU和内存配置进行优化。例如,许多.NET应用程序将在32位或64位代码中运行,具体取决于它们被JIT编译的位置。在64位硬件上,它们将使用更多的寄存器、内存和更好的指令集。
  • 紧密循环内部的虚方法调用可以根据对引用类型的运行时知识替换为静态调用。

我认为未来会有突破。特别是,我认为JIT编译和动态类型的结合将得到显着改进。我们已经在JavaScript领域看到了这一点,Chrome的V8和TraceMonkey就是如此。我期待在不久的将来看到其他类似于此的改进。这很重要,因为即使所谓的“静态类型”语言也倾向于具有许多动态特性。


8
您只提到JIT编译器可以做什么,而没有说明它实际上是如何运作的。JIT编译通常受限于必须快速执行的限制。离线编译器可以花费很长时间来优化代码,但JIT编译器必须在一两秒内完成。因此,两者都有其优势。 - jalf
13
正如我在帖子中明确指出的那样,HotSpot实际上进行动态的、基于性能分析的重新编译,而.NET实际上进行CPU特定的编译。我怀疑这两者都不会对启动速度产生影响(深思熟虑一下)。虚拟调用优化?看看V8就好了。我同意它们都有优点。 - Craig Stuntz
13
JIT编译器不仅可以根据运行时观察代码的使用情况来优化代码,而且可以使用更为激进("危险")的优化策略,因为当优化假设不再有效时,它可以通过丢弃已编译的代码来撤销优化。Hotspot就是这样做的。 - Ken Bloom
4
基于运行时特征,虚方法调用不仅可以被替换为静态调用,而且还可以根据相同的运行时特征进行内联。 - Ken Bloom
1
所有这些都可以通过AOT编译器完成(例如跟踪运行时行为并切换到替代代码路径,在安装软件时使用AOT以针对特定机器进行优化,将分支从内部循环中基本“提升”,编译器已经做了半个世纪等)。 - Brendan

15

是的,JIT编译器可以生成针对当前环境优化的更快的机器码。但实际上,虚拟机程序比原生程序慢,因为JIT本身需要消耗时间(更多的优化==更长的时间),并且对于许多方法,JIT可能会消耗比执行它们更多的时间。这就是为什么在.NET中引入了GAC。

JIT的一个副作用是占用大量内存。然而,这与计算速度无关,它可能会减慢整个程序的执行,因为大量的内存消耗增加了代码被分页到二级存储的概率。

对于我的糟糕英语请谅解。


我认为你指的是NGEN,而不是GAC。 - Craig Stuntz
2
GAC代表全局程序集缓存 http://en.wikipedia.org/wiki/Global_Assembly_Cachengen是用于将程序集添加到其中的工具 - Bahaa Zaid
通常情况下,JIT编译器不会将已编译的代码分页出去,而是会丢弃最近未使用的编译代码,并在需要时重新编译它们。 - Eclipse
我知道它们是什么。关键是NGEN通过预编译实际上增加了速度。 - Craig Stuntz
4
@gacutil用于将程序集添加到全局程序集缓存中,而NGEN用于将程序集添加到本机映像缓存中。 - Dave Van den Eynde

9
JIT有优势,但我不认为它会完全接管。传统编译器可以花更多的时间进行优化,而JIT需要在太多优化(花费的时间超过了优化所节省的时间)和太少优化(在直接执行中花费太多时间)之间找到平衡点。
显而易见的答案是在每个方面都使用优势。 JIT比传统优化器更容易利用运行时分析(虽然有编译器可以将运行时分析作为优化指导的输入),并且通常可以承担更多的CPU特定优化(同样,许多传统编译器也可以实现这一点,但如果您希望在不同系统上运行可执行文件,则无法充分利用它)。传统编译器可以花更多时间,并以不同的方式完成。
因此,未来的语言系统将具有良好的优化编译器,该编译器将发出专为优化JIT编译器设计的可执行代码。(对于许多人来说,这也是当前的语言系统。)(未来的语言系统还将支持从现代Python / VB脚本到最丑陋的高速数字计算等所有内容。)
像许多事物一样,这是由Lisp预示的。相当长一段时间以前,一些Lisp系统(不能真正说很多,因为没有那么多Common Lisp实现)通过即时编译来解释Lisp函数。Lisp S表达式(代码编写的内容)是解析树的相当简单的描述,因此编译可以非常快速。同时,优化Lisp编译器可以预先处理真正重要的性能代码。

对于混合型编程,我给予肯定。顺便说一句,NGEN大多数情况下可以实现你所描述的功能。 - Craig Stuntz

3

JIT编译器是否有比其他编译器如C ++更快的情况?

当然可以。例如,如果您找到了一个非常糟糕的AOT编译器和一个极好的JIT编译器,则自然可以预期这两种实现中JIT会更快。

您认为未来JIT编译器只会看到轻微的优化、功能,但性能类似,还是会有突破使其无限超越其他编译器?

不会。相反,后者更有可能发生。

从历史上看,大多数AOT的实现都是由开发人员使用(而不是最终用户),导致他们对通用目标进行优化(例如,“所有64位80x86,拥有谁知道多少RAM”的类型),而不是最终用户的特定硬件(例如,“带有16 GiB DDR3-2400 RAM的AMD Ryzen模型2345”); 而软件被分解为“编译单元”,它们被单独优化(以创建对象文件),然后链接而没有进一步优化。这造成了主要优化障碍,阻止了AOT实现其所能实现的性能。

近年来,出现了朝向整个程序优化的推动(以链接时优化和/或链接时代码生成的形式)以打破其中之一的优化障碍。

为了突破另一个优化障碍(在编译时不知道特定目标),一些编译器(例如英特尔的ICC)会为某些代码的多个版本生成代码,并在运行时选择要使用的版本。还有一些情况下发生“安装时间AOT”(例如Gentoo Linux);以及一些情况下开发人员提供许多单独的二进制文件(针对许多不同的目标进行了优化),而安装程序则选择要下载/安装的二进制文件。

优化的另一个障碍来自于使用-例如改变代码以更好地适应给定的数据。没有什么可以阻止AOT为不同的场景生成不同的代码,并根据运行时数据选择要使用的版本。最简单最常见的情况是“memcpy()”,您可以期望复制数据的代码有多个版本,使用的版本是基于要复制的数据量进行选择的。使用足够先进的AOT(可能与分析器指导的优化结合使用),这种技术可能会变得极其复杂。

基本上,这些(AOT)优化的“历史性障碍”并不存在于JIT中,这也是为什么JIT可以接近AOT的性能的原因;而AOT将继续寻找避免/修复这些障碍的方法,并且AOT将增加其对JIT的性能优势。

另一方面,JIT不能像整个程序优化那样工作(除非成为AOT的一种形式),因此无论JIT编译器变得多么先进,也永远无法像AOT那样变得出色。

此外,在运行时修改或生成代码会对现代CPU的性能产生影响(由于破坏了预测执行并需要特殊串行化,除了污染跟踪高速缓存和分支预测数据等原因);对于现代多线程软件而言更糟糕,因为JIT编译器还需要确保代码在所有CPU上是一致的;而确保代码可以修改也有"持久成本"(例如使用间接调用而不是直接调用,以便在JIT完成后可以原子更新到指向不同代码段的单个位置),即使代码没有被修改并且所有JITting已经完成。

对于(虚拟)内存消耗而言,JIT也更糟糕 - 而不是程序的代码和程序的数据;你拥有程序的原始代码和程序的数据,加上程序的JIT代码、JIT编译器的代码和JIT编译器的数据。随着JIT编译器变得更加先进,它们会消耗更多的内存,并且内存消耗将变得更加糟糕。更高的内存消耗也会稍微降低性能(由于在较低层次管理内存时存在“非O(1)”的开销 - 页面表等),但也会将程序推向“内存不足”的状态(例如交换空间使用、内存分配失败和某些操作系统中的“OOM killer”)。

当然,大多数系统都是多任务的,并且全局资源由多个进程共享;这意味着如果一个进程使用更多的CPU时间和内存,则其他完全不相关的进程(以及操作系统的不相关部分 - 例如文件数据缓存)就会有较少的资源可用。即使JIT的低效对于一个进程没有关系(那个进程仍然“足够快”,并且自身不会耗尽内存),它也会影响其他所有进程,并且可能对所有其他进程都很重要。

换句话说,如果你正在比较具体的实现,那么JIT可能会更好(或类似,或更差),但这是一项实现细节,并不是因为JIT本身更好。任何公平的比较JIT和AOT的基准测试都必须依赖于特定的实现,并暗示所使用的AOT编译器的实现可以和应该得到改进(并不意味着JIT可以和AOT一样好,或者应该变得像AOT一样好)。

然而...

或者会不会出现突破,使其相比其他编译器无限制优越?

这实际上取决于你对“优越”的定义是什么。JIT有一个巨大的优势,即开发人员等待代码编译的时间较少(在渴望利润的情况下,高开发成本意味着最终产品的成本更高)。

多年来/几十年来,在许多领域中,我们已经看到了一种向牺牲最终结果的质量/效率以降低开发时间/成本的趋势(例如转向“Web应用程序”)。我们也在不相关的市场上看到了相同的趋势(为了好玩,请尝试找到简单的衣物夹,它们不是廉价而恶心的塑料,在阳光下几年后就

最终,除非有法律规定的质量标准(主要是为了安全原因 - 如汽车、医疗设备、电气和易燃气体等),否则“低质量、低成本”将获胜;而且(由于几乎所有软件都没有任何质量标准),可以合理地假设JIT将变得更加占主导地位,因为它更便宜(尽管质量较低/性能较差)。

1

在这次对话中被忽略的另一件事是,当您JIT代码时,它可以编译到内存中的一个空闲位置。在像C++这样的语言中,如果DLL基于这样的方式,即该内存块不可用,则必须经过昂贵的重新定位过程。将代码JIT到未使用的地址中比将已编译的DLL重新定位到空闲内存空间更快。更糟糕的是,重新定位的DLL不能再共享。(请参见http://msdn.microsoft.com/en-us/magazine/cc163610.aspx

我对C# 3.5 JIT代码中的一些优化并不是很满意。像压缩所必需的位操作非常低效(它拒绝在CPU寄存器中缓存值,而是为每个操作都访问内存)。我不知道为什么会这样做,但这会产生巨大的差异,而我无能为力。

个人认为一个好的解决方案是可以设置优化级别(1-100),告诉JIT编译器你觉得它应该花多少时间来优化你的代码。另一个解决方案就是AOT(提前编译)编译器,但这样会失去许多JIT代码的优势。


重新定位并不像你所说的那么慢。相对于在首选地址加载,它确实会慢一些。相对于JIT,就难以确定了。而且由于JIT代码永远不会共享,这是另一种情况下AOT胜出的情况,直到你遇到一个加载地址冲突。 - Ben Voigt

1
很多人回答说我可能浏览太快了(或者我可能理解有误),但对我来说,它们是两个不同的东西:
据我所知,没有什么能阻止你JIT编译的C++代码,例如Dynamo JIT后生成的机器代码:

http://arstechnica.com/reviews/1q00/dynamo/dynamo-1.html

实际上,在某些情况下,它确实提供了速度改进。

编译代码,从C++编译器的角度来看,意味着将用一种语言编写的代码转换为一组指令(或在某些情况下转换为另一种语言,然后再次进行编译),以便可以由某种逻辑设备执行。

例如,C++编译为汇编语言(我猜这样?), 或者C#编译为IL 或者Java编译为字节码

JIT是在执行过程中发生的过程。执行代码的机器分析代码以查看是否可以改进它。Java和C#都能够同时使用编译器为虚拟机准备命令,然后虚拟机至少有机会尝试优化它。

有些程序不是编译的,而是解释的,这意味着运行它们的机器会读取您编写的确切代码。这些机器有机会进行一些JIT,但要记住它们也可以静态编译,潜在地成为原始语言设计者意料之外的第三方供应商。

所以,回答你的问题,我不认为JIT会取代静态编译器。我认为至少在编程存在的时间内,总会有一种将程序的表示转换为某种类型的机器指令集的地方(在此过程中可能进行优化)。

然而,我认为JIT可能会成为故事的更大一部分,随着Java运行时和.net运行时的发展,我相信JIT会变得更好,而且考虑到像Dynamo项目这样的东西,我想硬件也有可能采用JIT,这样你处理器所做的一切都是基于运行时环境重新优化的。


1

JIT编译器比静态编译器更了解系统。在机器上动态添加多线程可以大大提高速度,一旦它们开始工作。

通常情况下,JIT编译器具有一定的启动延迟,程序/代码的第一次运行可能比预编译代码慢得多。这是冷启动的劣势。

JIT编译的另一个重要优点是,在构建程序后,编译器可以进行更新并获得新的编译器技巧,而无需完全部署新程序。


1
一个有利于“离线”编译器的未提及的观点是,这样的编译器可以有用地针对RAM量较小的平台,甚至只有16字节。当然,任何稍微兼容PC的东西都可能比这多(字面上)数百万倍的RAM,但我认为,在找到具有许多兆字节RAM且成本低于$0.50并在操作期间消耗少于一毫瓦的机器之前,还需要一段时间。

请注意,16字节的RAM并不像听起来那么弱,因为具有如此小RAM的芯片不会将代码存储在RAM中 - 有一个单独的非易失性内存区域来保存代码(384字节是我所知道的最小值)。当然,这并不算多,但足以使$0.25的处理器执行需要$1.00离散元件才能完成的功能。


1
基本上,JIT编译器有机会实际分析正在运行的应用程序,并根据该信息进行一些提示。 "离线"编译器将无法确定分支跳转和落空的频率,除非插入特殊代码,请开发人员运行程序,使其通过测试并重新编译。
这为什么很重要?
//code before
if(errorCondition)
{
  //error handling
}
//code after

会被转换成类似这样的东西:

//code before
Branch if not error to Code After
//error handling
Code After:
//Code After

而x86处理器在没有来自分支预测单元的信息时不会预测有条件的跳转。这意味着它预测错误处理代码将运行,并且处理器在发现未发生错误条件时必须刷新流水线。

JIT编译器可以看到这一点,并插入分支提示,以便CPU能够正确预测。当然,离线编译器可以以一种避免mispredict的方式构造代码,但如果您需要查看汇编代码,您可能不喜欢它跳来跳去...


1
我知道 - “离线编译器...无需插入特殊代码,要求开发人员运行程序,测试并重新编译。” - Calyth
Nils,JIT编译器可以实时对数据集进行性能分析,而静态编译器仍然受限于静态数据集。 - kchoi

1
JIT编译的一个优点是程序可以定义无限数量的通用类型,例如:
interface IGenericAction { bool Act<T>(); }

struct Blah<T>
{
  public static void ActUpon(IGenericAction action)
  {
     if (action.Act<T>())
       Blah<Blah<T>>.ActUpon(action);
  }
}

调用Blah<Int32>.ActUpon(act)将调用act.Act<Int32>()。如果该方法返回true,则会调用Blah<Blah<Int32>>.ActUpon(act),该方法将进而调用act.Act<Blah<Int32>>()。如果返回true,则将使用嵌套更深的类型执行更多调用。生成所有可能被调用的ActUpon方法的代码是不可能的,但幸运的是这不是必要的。在使用之前不需要生成类型。如果action<Blah<...50 levels deep...>>.Act()返回false,则Blah<Blah<...50 levels deep...>>.ActUpon不会调用Blah<Blah<...51 levels deep...>>.ActUpon,后一种类型也不需要被创建。

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