Loading AI tools
一种执行计算机代码的方法 来自维基百科,自由的百科全书
在计算机技术中,即时编译(英語:Just-in-time compilation,缩写为JIT;又译及时编译[1]、实时编译[2]),也称为动态翻译或运行时编译[3],是一种执行计算机代码的方法,这种方法设计在程序执行过程中(在執行期)而不是在执行之前进行编译。[4]通常,这包括源代码或更常见的字节码到机器码的转换,然后直接执行。实现 JIT 编译器的系统通常会不断地分析正在执行的代码,并确定代码中可被即时编译加速的部分,在这些部分中,由编译或重新编译带来的性能提高将超过编译该代码的开销。
JIT编译是两种传统的机器代码翻译方法——提前编译(AOT)和直譯器——的结合,它结合了两者的优点和缺点。[4]大致来说,JIT编译,以解释器的开销以及编译和链接(解释之外)的开销,结合了编译代码的速度与解释的灵活性。JIT编译是動態編譯的一种形式,允许自适应优化,比如动态重编译和特定于微架構的加速[nb 1][5]——因此,在理论上,JIT编译比静态编译能够产生更快的执行速度。解释和JIT编译特别适合于动态编程语言,因为运行时系统可以处理后期绑定的数据类型并实施安全保证。
JIT编译可以应用于某些程序,也可以用于某些能力,特别是动态能力,如正则表达式。例如,一个文本编辑器可以把运行时提供的正则表达式编译成机器码,从而更快地进行匹配——这不能提前完成,因为pattern只在运行时提供。一些现代的运行时环境依赖JIT编译来实现高速代码执行,包括大多数Java实现,以及微软的.NET框架。类似地,许多正则表达式库都具有对正则表达式进行JIT编译的功能,可以编译成字节码,也可以编译成机器码。JIT编译也用于一些模拟器中,以便将机器代码从一个CPU体系结构转换到另一个CPU体系结构。
JIT编译的一个常见实现是首先进行AOT编译,把源代码编译成字节码(虚拟机代码),称为字节码编译,然后将JIT编译为机器码(动态编译),而不是解释字节码。与解释相比,这提高了运行时性能,但代价是编译造成的延迟。与解释器一样,JIT编译器不断地进行翻译,但是对编译后的代码进行缓存可以最大限度地减少在给定运行期间将来执行相同代码的延迟。
在字节码编译的系统中,源代码被转换为称为字节码的中间表示形式。字节码不是任何特定计算机的机器代码,可以在计算机体系结构之间移植。然后可以在虚拟机上解释或运行字节码。JIT编译器在许多部分(或全部、很少)读取字节码,并将它们动态编译成机器代码,以便程序能够更快地运行。这可以针对每个文件、每个函数甚至任何任意代码片段进行编译; 代码可以在即将执行时进行编译(因此称为“即时”),然后缓存并在以后重用,无需重新编译。
相比之下,传统的解释型虚拟机只解释字节码,通常性能要低得多。有些解释器甚至不需要首先编译成字节码就可以解释源代码,但性能更差。静态编译的代码或本地代码在部署之前编译。动态编译环境是在执行期间可以使用编译器的环境。 使用JIT技术的一个共同目标是达到或超过静态编译的性能,同时保持字节码解释的优势:解析原始源代码和执行基本优化的许多“繁重工作”通常是在编译时处理的,在部署之前:从字节码编译到机器码要比从源代码编译快得多。与本地代码不同,部署的字节码是可移植的。由于运行时可以控制编译,比如解释字节码,所以它可以在安全的沙箱中运行。从字节码到机器码的编译器更容易编写,因为便携式字节码编译器已经完成了大部分工作。
JIT代码通常比解释器性能更好。另外,在某些情况下,它的性能可以比静态编译更好,因为许多优化只在运行时可行:[6][7]
由于JIT必须在运行时呈现和执行本地二进制映像,因此真正的机器代码JIT需要允许在运行时执行数据的平台,这使得在基于哈佛结构的机器上使用这种JIT成为不可能的事情——对于某些操作系统和虚拟机也是如此。然而,一种特殊类型的“JIT”可能并不针对物理机器的CPU体系结构,而是一种优化的VM字节码,在这种情况下,对原始机器代码的限制占了上风,特别是在字节码的VM最终将JIT用于本机代码的情况下。[8]
由于加载和编译字节码所需的时间,JIT在应用程序的初始执行中会导致轻微到明显的延迟。有时这种延迟被称为“启动时间延迟”或“预热时间”。一般来说,JIT执行的优化越多,生成的代码就越好,但是初始延迟也会增加。因此,JIT编译器必须在编译时间和希望生成的代码质量之间进行权衡。除了JIT编译之外,IO绑定操作也会增加启动时间:例如,JVM的“rt.jar”类数据文件为40 MB,JVM必须在这个巨大的上下文文件中寻找大量数据。[9]
Sun的HotSpot Java虚拟机使用的一种可能的优化方法是将解释和JIT编译结合起来。应用程序代码最初是被解释的,但JVM监视哪些字节码序列经常被执行,并将它们转换为机器代码,以便在硬件上直接执行。对于只执行几次的字节码,这节省了编译时间并减少了初始延迟;对于频繁执行的字节码,JIT编译用于在缓慢解释的初始阶段之后以高速运行。此外,由于程序花费大量时间执行的其实只是一小部分代码,因此减少的编译时间非常重要。最后,在初始代码解释期间,可以在编译之前收集执行统计信息,这有助于执行更好的优化。[10]
正确的权衡可以根据具体情况而变化。例如,Sun的Java虚拟机有两种主要模式: 客户机和服务器。在客户端模式下,执行最小程度的编译和优化,以减少启动时间。在服务器模式下,将执行大量的编译和优化,以牺牲启动时间来最大限度地提高应用程序运行时的性能。其他Java即时编译器使用一个方法执行次数的运行时度量,结合方法的字节码大小作为一种启发式方法来决定何时编译。[11]还有的使用执行的次数与检测循环相结合。[12]一般来说,在短期运行的应用程序中准确预测要优化的方法要比在长期运行的应用程序中准确得多。[13]
微软的本地镜像生成器(Ngen)是另一种减少初始延迟的方法。[14]Ngen将通用中间语言映像中的字节码预编译成机器本机代码。因此,不需要运行时编译。Visual Studio 2005附带的.NET Framework 2.0在安装之后立即在所有微软库dll上运行Ngen。预JIT提供了一种提高启动时间的方法。但是,它生成的代码质量可能不如JIT生成的代码质量好,原因与静态编译的代码(没有按配置优化)在极端情况下不如JIT编译的代码的原因相同:缺乏分析数据来驱动,例如,内联缓存。[15]
还有一些Java实现将AOT编译器与JIT编译器(Excelsior JET)或解释器(GNU Compiler for Java)结合起来。
最早发布的JIT编译器通常归功于约翰·麦卡锡在1960年对LISP的研究。[16]在他的重要论文《符号表达式的递归函数及其在机器上的计算》(Recursive functions of symbolic expressions and their computation by machine, Part I)第一部分中,他提到了在运行时被转换的函数,因此不需要保存编译器输出来打孔卡[17](虽然更准确的说法是“编译并执行系统”)。另一个早期的应用来自肯·汤普逊,他在文本编辑器QED的正则表达式模式匹配中使用了JIT。[18]为了提高速度,Thompson在相容分時系統上通过JIT到IBM 7090代码实现了正则表达式匹配。[16]1970年,Mitchell首创了一种有影响力的从解释中获取编译代码的技术,他在实验语言LC²中实现了这种技术。[19][20]
Smalltalk(1983年)开创了JIT编译的新领域。例如,按需翻译为机器代码,缓存结果以供以后使用。当内存不足时,系统会删除部分代码,并在需要时重新生成。[4][21]Sun的Self语言广泛地改进了这些技术,一度是世界上速度最快的Smalltalk系统;运用完全面向对象的语言实现了高达优化C语言一半的速度。[22]
Self被Sun抛弃了,但是研究转向了Java语言。“即时编译”这个术语是从制造术语“及时”中借来的,并由Java普及,James Gosling从1993年开始使用这个术语。[23]目前,大多数Java虚拟机的实现都使用JIT技术,因为HotSpot建立在这个研究基础之上,而且使用广泛。
HP的项目Dynamo[24]是一个实验性的JIT编译器,其字节码格式和机器代码格式是相同的;该系统将PA-6000机器代码转换为PA-8000机器代码。与直觉相反,这导致了速度的提高,在某些情况下是30%,因为这样做允许在机器代码级别进行优化,例如,内联代码以更好地使用缓存,优化对动态库的调用,以及许多其他常规编译器无法尝试的运行时优化。[25][26]
JIT编译从根本上使用可执行数据,因此带来了安全挑战和可能的漏洞。
JIT编译的实现包括将源代码或字节码编译成机器码并执行它。这通常是直接在内存中完成的——JIT编译器将机器代码直接输出到内存中并立即执行,而不是像通常的提前编译那样将其输出到磁盘,然后作为单独的程序调用代码。在现代的体系结构中,由于可执行空间保护,这会遇到一个问题——无法在任意内存裡執行程式。在任意内存裡執行程式存在潜在的安全漏洞。因此,必须将内存标记为可执行;出于安全原因,应在代码写入内存并标记为只读之后执行,因为可写/可执行内存是一个安全漏洞(参见W^X)。[29]例如 Firefox 中的 JavaScript JIT 编译器在 Firefox 46 版本中引入了这种保护。[30]
JIT喷射是一种利用漏洞利用的技术,它使用JIT编译进行堆喷射——生成的内存然后是可执行的,如果执行可以移动到堆積中,这就允许利用。