三种编译技术比较_比较编译技术

Java应用程序性能有时在开发社区中引起了激烈的争论。 因为设计该语言是为了支持应用程序可移植性的关键目标而设计的,所以早期的Java运行时所提供的性能水平大大低于诸如C和C ++之类的编译语言所能达到的水平。 尽管此类语言可以在更高级别上执行,但是生成的代码只能在有限数量的系统上执行。 在过去的十年中,Java运行时供应商已经开发了复杂的动态编译器,通常称为即时(JIT)编译器。 JIT编译器在程序运行时有选择地将执行频率最高的方法编译为本机代码。 像使用C或C ++编写的程序一样,将本机代码编译延迟到运行时而不是在程序运行之前进行编译可以满足可移植性要求。 一些JIT编译器甚至不使用解释器就编译所有代码,但是这些编译器仍通过在程序执行时进行操作来保留Java应用程序的可移植性。

得益于动态编译技术的许多进步,现代的JIT编译器可以产生与各种应用程序的C或C ++静态编译性能相匹配的应用程序性能。 尽管如此,许多软件开发人员仍根据经验或轶事证据认为动态编译会严重干扰程序操作,因为编译器必须与应用程序共享CPU。 一些开发人员坚决认为要解决此性能问题,便强烈要求Java代码进行静态编译。 的确,对于某些应用程序和执行环境,静态编译可以极大地提高Java性能,或者是唯一的实用选择。 但是,静态编译Java应用程序时,要获得良好的性能会涉及许多复杂性。 普通的Java开发人员可能没有完全意识到动态JIT编译器的优势。

本文着眼于静态和动态地编译Java语言所涉及的一些问题,重点是对实时(RT)系统的影响。 它简要描述了Java语言解释器的操作方式,然后描述了由现代JIT编译器执行的本机代码编译的优缺点。 它介绍了IBM®在WebSphere®Real Time中发布的AOT编译技术,并介绍了它的一些优缺点。 然后,它比较和对比了两种编译策略,并指出了几个应用程序区域以及AOT编译可能是更好方法的执行环境。 要点是,这两种编译技术不是互斥的:两者的优缺点都影响每种技术最有效的应用程序类型。

执行一个Java程序

最初,通过Java SDK的javac程序将Java程序编译为与平台javac的本地格式,即类文件。 可以将这种格式视为Java平台,因为它定义了执行用Java语言编写的程序所需的所有信息。 Java程序的执行引擎(也称为Java运行时环境(JRE))包括一个虚拟机,该虚拟机为特定的本机平台实现Java平台。 例如,在AIX®操作系统上运行的基于Linux®的Intel®x86平台,Sun Solaris平台和IBM System p™平台均具有JRE。 这些JRE实现实现了正确执行为Java平台编写的程序所需的所有本机支持。

Java平台程序表示的一个重要部分是一系列字节码 ,它们描述Java类中每个方法执行的操作。 字节码描述使用理论上无限大的操作数堆栈的计算。 这种基于堆栈的程序表示法提供了平台中立性,因为它不依赖于任何特定本机平台的CPU中可用的寄存器数。 可以独立于任何本机处理器的指令集定义在操作数堆栈上可以执行的操作。 这些字节码的执行由Java虚拟机(JVM)规范定义(请参阅参考资料 )。 执行Java程序时,用于任何特定本机平台的任何JRE必须遵守JVM规范规定的规则。

由于很少有基于堆栈的本机平台(Intel X87浮点协处理器是一个明显的例外),因此大多数本机平台无法直接执行Java字节码。 为了解决这个问题,早期的JRE通过解释字节码来执行Java程序。 也就是说,JVM反复循环运行:

  1. 获取下一个要执行的字节码。
  2. 对其进行解码。
  3. 从操作数堆栈中获取所需的操作数。
  4. 根据JVM规范执行操作。
  5. 将任何结果写回堆栈。

这种方法的优点是简单:JRE开发人员只需编写代码即可处理每种类型的字节码。 并且由于少于255个字节码可用于描述操作,因此实现成本较低。 当然,缺点是性能:尽管Java平台有许多其他优点,但许多人还是在早期就使用它来谴责Java平台。

解决诸如C或C ++之类的性能差距意味着要以不牺牲可移植性的方式为Java平台开发本机代码编译。

编译Java代码

尽管有轶事证据表明Java编程的“一次写入,遍地运行”的口号可能并非在所有情况下都严格符合要求,但它确实适用于各种应用程序。 另一方面,本机编译本质上是特定于平台的。 那么Java平台如何在不牺牲平台中立性的情况下实现本机编译性能? 答案是并且已经十年来以JIT编译器的形式进行动态编译(参见图1):

图1. JIT编译器
图1. JIT编译器

使用JIT编译器,Java程序在执行到本机处理器的指令中时可以一次编译一种方法,以实现更高的性能。 该过程涉及生成一种方法的内部表示形式,该方法与字节码不同,但其级别高于目标处理器的本机指令。 (IBM JIT编译器使用一系列表达树来表示方法的操作。)编译器执行一系列优化以提高质量和效率,最后执行代码生成步骤,将优化的内部表示形式转换为目标处理器的本机指令。 生成的代码依赖于运行时环境来执行活动,例如确保类型转换合法或分配某些类型的对象,这些对象在代码本身中无法直接执行。 JIT编译器在与应用程序线程分离的编译线程上运行,因此应用程序不需要等待编译发生。

图1中还描绘了一个分析框架,该框架通过定期采样线程以查找频繁执行的方法来观察执行程序的行为。 它还提供了用于方法的专业分析版本的工具,以存储在程序执行过程中可能不会更改的动态值。

由于此JIT编译过程是在程序执行时发生的,因此可以保持平台中立性:中立的Java平台代码仍然是分发形式。 诸如C和C ++之类的语言缺乏此优势,因为它们的本机编译步骤是在程序执行之前执行的; 本机代码是分发到(本机平台)执行环境的内容。

挑战性

尽管通过JIT编译可以保持平台中立,但这是有代价的。 由于编译与程序执行同时进行,因此编译代码所花费的时间会添加到程序的运行时间中。 正如曾经构建过简单的C或C ++程序的任何人都可以建立联系,编译通常不是一个快速的过程。

为了解决这个缺点,现代的JIT编译器采用两种方法之一(在某些情况下,两者都采用)。 第一种方法是编译所有代码,但不执行任何昂贵的分析或转换,以便快速生成代码。 可以如此快速地生成代码,以至于从编译中观察到的开销尽管很明显,但很容易被隐藏在重复执行本机代码导致的性能改进后面。 第二种方法是将编译资源仅分配给少数经常执行的方法(通常称为热方法)。 维护了较低的编译开销,可以更容易地将其隐藏在重复执行热代码所带来的性能优势后面。 许多应用程序只花时间执行少量热方法,因此这种方法有效地降低了编译的性能成本。

动态编译器的基本复杂性是,在需要知道方法的执行对整个程序的性能有多大贡献的需求与编译代码的预期收益之间取得平衡。 举一个极端的例子,在程序执行之后,您将完全了解哪些方法对此特定执行的贡献最大,但是编译这些方法没有任何价值,因为程序已经完成。 另一方面,在程序执行之前,尚无关于哪些方法重要的知识,但每种方法的潜在利益却已最大化。 大多数动态编译器通过平衡了解重要内容的需求和该知识的预期收益,在这两种极端之间进行操作。

Java语言要求动态加载类这一事实对Java编译器的设计产生了重大影响。 如果编译的代码引用了另一个尚未加载的类怎么办? 一个示例可能是一种方法,该方法读取尚未加载的类的静态字段的值。 Java语言要求第一次执行对类的引用会导致将该类加载并解析到当前JVM中。 在第一次执行之前,该引用尚未解析,这意味着没有地址可从中加载该静态字段。 编译器如何处理这种可能性? 如果尚未加载该类,则编译器将生成导致该类加载和解析的代码。 解析完类后,将以线程安全的方式修改原始代码位置,以直接访问静态字段的地址,因为这样就知道了该地址。

IBM JIT编译器已经进行了大量工作,以使用安全但有效的代码修补技术,以便在类解析之后,执行的本机代码简单地加载字段的值,就好像在编译时已解析了该字段一样。 另一种方法是生成代码,该代码在查找字段所在位置然后加载值之前始终检查该字段是否已解析。 对于已解决且经常访问的未解决字段,这种幼稚的过程可能是一个巨大的性能问题。

动态编译的好处

动态编译Java程序具有重要的好处,与静态编译的语言相比,可以产生更好的代码。 现代的JIT编译器通常在生成的代码中插入钩子,以收集有关程序行为方式的信息,这样,如果选择了重新编译方法,则可以更好地优化动态行为。

这种方法的一个很好的例子是收集特定arraycopy操作的长度。 如果发现每次执行时该长度几乎都是恒定的,则可以生成最常用的arraycopy长度的专用代码,或者可以调用针对该长度更好地调整的代码序列。 由于存储系统和指令集设计的本质,复制存储器的最佳通用例程很少与复制特定长度的代码一样快。 例如,复制8个字节的对齐数据可能需要直接复制一条或两条指令,而使用能够处理任何对齐方式的任意数量字节的通用复制循环,复制多达8个字节的相同8个字节可能需要10条指令。 但是,即使针对一个特定长度生成了此类专用代码,生成的代码也必须正确执行针对其他长度的副本。 只是简单地生成代码以使其在通常观察到的长度上更快,从而平均而言可以提高性能。 对于大多数静态编译的语言来说,这种优化通常是不切实际的,因为对于所有可能的执行而言,恒定的长度比在一个特定程序执行中恒定的长度更为罕见。

这种优化的另一个重要示例是基于类层次的优化。 例如,虚拟方法调用涉及查看接收方对象的类以进行调用,以发现哪个实际目标为接收方对象实现了虚拟方法。 研究表明,大多数虚拟调用对于所有接收器对象都只有一个目标,并且JIT编译器可以为直接调用生成比虚拟调用更高效的代码。 通过分析编译代码时类层次结构的状态,JIT编译器可以找到用于虚拟调用的单个目标方法,并生成直接调用目标方法而不执行较慢的虚拟调用的代码。 当然,如果类层次结构发生了变化,并且有可能使用第二种目标方法,那么JIT编译器可以更正最初生成的代码,以便执行虚拟调用。 实际上,很少需要进行这些更正。 同样,进行此类校正的潜在需求使得执行此优化在静态上很麻烦。

因为动态编译器通常只将编译工作集中在少数几种热门方法上,所以可以执行更具攻击性的分析以生成甚至更好的代码,从而使编译的回报更高。 实际上,大多数现代的JIT编译器还支持重新编译方法,这些方法非常热门。 可以使用通常在静态编译器中发现的极为激进的优化方法(对编译时间的重视程度较低)对这些频繁执行的方法进行分析和转换,以生成更好的代码并提高性能。

这些改进以及其他类似改进的综合效果是,对于大量Java应用程序而言,动态编译弥合了差距,在某些情况下,甚至超过了针对C和C ++等语言的静态本机编译所能提供的性能。 。

缺点

尽管如此,动态编译的确有一些缺点,使其在某些情况下不是理想的解决方案。 例如,由于识别频繁执行的方法以及编译这些方法需要花费时间,因此应用程序通常会经历预热期,在此期间性能尚未达到最高峰。 由于许多原因,这个预热期可能是性能问题。 首先,大量的初始编译会直接影响应用程序的启动时间。 这些编译不仅会延迟应用程序达到稳定状态(想象Web服务器在达到能够执行有用工作之前要经过初始化阶段),而且在预热阶段频繁执行的方法可能不会起到很大作用应用程序的稳态性能。 进行JIT编译会延迟启动,但并不能显着提高应用程序的长期性能,这特别浪费。 尽管所有现代JVM都进行了调整以减轻启动惩罚,但仍不能在所有情况下都完全消除该问题。

其次,某些应用程序根本无法忍受与动态编译相关的延迟。 诸如GUI界面之类的交互式应用程序就是一个示例。 在这种情况下,编译活动可能会对用户的体验产生不利影响,而不会显着提高应用程序的性能。

最后,旨在在具有严格任务期限的实时环境中运行的应用程序可能无法忍受编译的不确定性性能影响或动态编译器本身的内存开销。

因此,尽管JIT编译技术已经发展到可以提供与静态语言性能相当甚至更好的水平的程度,但是动态编译根本不适合某些应用程序。 在这些情况下,Java代码的提前(AOT)编译可能是正确的解决方案。

AOT Java编译

原则上,Java语言的本机编译应该是为传统语言(如C ++或Fortran)开发的编译技术的直接应用。 不幸的是,Java语言本身的动态性质引入了其他复杂性,这些复杂性会影响Java程序的静态编译代码的质量。 但是基本思想还是一样的:在程序执行之前为Java方法生成本机代码,以便一旦程序运行就可以直接使用本机代码。 目的是避免JIT编译器的运行时性能或内存成本,或者避免解释器的早期性能开销。

挑战性

对于动态JIT编译器来说,动态类加载是一个挑战,对于AOT编译而言,它甚至是一个更为重要的问题。 在执行代码引用该类之前,无法加载该类。 由于AOT编译发生在程序执行之前,因此编译器无法做出关于已加载哪些类的任何假设。 这意味着即使对于直接(即非虚拟)调用,编译器也不知道任何静态字段的地址,任何对象的任何实例字段的偏移量或任何调用的真实目标。 对这些信息中的任何一个进行假设,当代码执行时,这些信息被证明是错误的,这意味着该代码是不正确的,并且已经牺牲了Java一致性。

因为代码可以在任何环境中执行,所以类文件可能与编译代码时的文件不同。 例如,一个JVM实例可能从磁盘上的特定位置加载一个类,而随后的实例可能从另一位置甚至通过网络加载该类。 想象一下正在进行错误修复的开发环境:类文件的内容可以从一个程序执行更改为另一个程序执行。 而且,直到程序运行,Java代码可能甚至不存在:例如,Java反射服务通常在运行时生成新类来支持程序的活动。

缺少关于静态,字段,类和方法的知识,这意味着Java编译器中的大多数优化框架都受到严重阻碍。 内联可能是静态或动态编译器所应用的最重要的优化方法,由于该编译器没有有关调用的目标方法的信息,因此不再可用。

因此,必须在未解析每个静态,字段,类和方法引用的情况下生成AOT代码。 在执行时,必须使用当前运行时环境的正确值来更新这些引用中的每个引用。 此过程可能会直接影响首次执行性能,因为所有引用都在第一次执行时就已解决。 当然,后续执行将受益于代码修补的结果,从而可以更直接地引用实例或静态字段或方法目标。

最重要的是,为Java方法生成的本机代码通常需要只能在单个JVM实例中使用的值。 例如,代码必须在JVM运行时中调用某些运行时例程以执行特定操作,例如查找未解决的方法或分配内存。 每次将JVM加载到内存中时,这些运行时例程的地址可以不同。 因此,AOT编译的代码需要先绑定到JVM的当前执行环境中,然后才能执行。 其他示例是字符串的地址和常量池条目的内部位置。

在WebSphere Real Time中,使用称为jxeinajar的工具执行AOT本机代码编译(请参见图2)。 该工具或者将本机代码编译应用于JAR文件中所有类的所有方法,或者选择性地将其应用于感兴趣的方法。 结果以一种称为Java eXEcutable(JXE)的内部格式存储,但是可以很容易地存储到任何持久性容器中。

图2. jxeinajar
图2. jxeinajar

您可能会认为静态编译所有代码是最好的方法,因为它会导致在运行时执行大量本机代码。 但是,这里可以进行一些折衷。 编译的方法越多,代码占用的内存就越多。 编译的本机方法大约比字节码大10倍:本机代码本身不如字节码密集,并且必须包含有关代码的其他元数据,以便可以将代码绑定到JVM中,并在发生异常或堆栈时正确执行要求跟踪。 组成普通Java应用程序的JAR文件通常包含许多很少执行的方法。 编译这些方法会带来内存损失,而且预期收益很少。 这种大小损失带来了相关的成本,即将代码存储在磁盘上,将代码从磁盘带出并进入JVM以及将代码绑定到JVM。 除非代码执行多次,否则这些成本可能不会被本机代码与解释的性能优势所抵消。

解决大小问题的原因是,在已编译方法和已解释方法之间进行调用(也就是说,当已编译方法调用已解释方法时,反之亦然)可能比从已解释方法到已解释方法或从已编译方法到已编译方法的调用更为昂贵。 。 动态编译器通过最终编译JIT编译代码经常调用的所有解释方法来减轻这种开销,但是如果没有动态编译器,就无法隐藏这种开销。 因此,如果方法是有选择地编译的,则必须注意最小化从已编译方法到未编译方法的过渡。 为所有可能的执行选择正确的方法集来避免此问题可能很困难。

好处

尽管AOT编译的代码具有我们概述的缺点和挑战,但是提前编译Java程序可以带来性能优势,尤其是在动态编译器并不总是有效的解决方案的环境中。

您可以通过仔细使用AOT编译的代码来加快应用程序的启动速度,因为尽管该代码通常比JIT编译的代码慢,但比解释的速度快很多倍。 此外,由于加载和绑定AOT编译的代码的时间通常少于检测和动态编译重要方法的时间,因此您可以在程序执行之前更早地实现该性能。 同样,交互式应用程序可以快速受益于本机代码性能,而无需付出动态编译的代价,而动态编译会导致响应速度变慢。

RT应用程序还可以从AOT编译的代码中获得重要好处:更具确定性的性能超过了解释后的性能。 WebSphere Real Time使用的动态JIT编译器特别适合在RT系统中使用。 它使编译线程的优先级比RT任务低,并且进行了优化以避免生成具有严重不确定性的性能影响的代码。 但是,在某些RT环境中,甚至JIT编译器的存在也是不可接受的。 这样的环境通常需要对截止日期管理进行最严格的控制。 在这些情况下,AOT编译的代码可以提供比解释的代码更好的原始性能,而不会影响可以实现的确定性。 消除JIT编译线程甚至可以消除在必须启动优先级更高的RT任务时抢占它的性能影响。

计分卡

动态(JIT)编译器通过利用应用程序执行的动态行为以及有关已加载类及其层次结构的知识来支持平台中立性并生成高质量的代码。 但是,JIT编译器的编译时预算有限,并且会影响程序的运行时性能。 另一方面,静态(AOT)编译器会牺牲平台中立性和代码质量,因为它们无法利用程序的动态行为,也不具备有关已加载类或类层次结构的任何知识。 AOT编译实际上具有无限的编译时间预算,因为AOT编译时间对运行时性能没有影响,尽管在实践中,开发人员不会永远等待静态编译步骤。

表1总结了本文讨论的用于Java语言的动态和静态编译器的几个特征:

表1.比较编译技术
动态(JIT) 静态(AOT)
平台中立 没有
代码质量
利用动态行为 没有
知识的阶级和等级 没有
编译时预算 有限,具有运行时成本 更少的限制,没有运行时成本
对运行时性能的影响 没有
编译什么 需要护理,由JIT处理 需要照顾,由开发商处理

两种技术都需要仔细选择要编译的方法以获得最高性能。 对于动态编译器,由编译器自己做出决定,而对于静态编译器,选择权由开发人员决定。 让JIT编译器选择要编译的方法可能不是优势,这取决于在特定情况下编译器的启发式方法的工作情况。 在大多数情况下,我们认为这是有益的。

因为JIT编译器可以最佳地优化正在运行的程序,所以它更适合于为大量生产Java系统提供最重要的稳态性能。 静态编译可以最好地覆盖交互式性能,因为没有运行时编译活动会干扰用户的响应时间期望。 可以通过调整动态编译器来在某种程度上解决启动和确定性性能问题,但是静态编译可以在需要时提供最快的启动速度和最高的确定性。 表2比较了四种不同执行环境中的两种编译技术:

表2.每种技术最好的地方
动态(JIT) 静态(AOT)
启动表现 可调,但不是很好 最好
稳态表现 最好
互动表演 不太好
确定性绩效 可调,但不是最好的 最好

图3显示了启动性能和稳态性能的总体趋势:

图3.性能AOT与JIT
图3.性能AOT与JIT

最初,JIT编译器的性能非常低,因为最初会解释方法。 随着更多的方法被编译,JIT花费更少的时间进行编译,性能曲线逐渐增大,最终开始达到峰值性能。 另一方面,AOT编译的代码开始时比解释的性能要高得多,但不太可能像通过JIT编译器所能达到的那样高。 将静态代码绑定到JVM实例会产生一些成本,因此性能最初会低于其稳态值。 但是,与使用JIT编译器相比,达到稳态水平要快得多。

没有一种本机代码编译技术适合所有Java执行环境。 每种技术通常都是强项,而另一项是弱项。 因此,两种编译技术都必须满足Java应用程序开发人员的需求。 实际上,可以将静态和动态编译一起使用以最大程度地提高性能-但前提是平台中立性(Java语言的主要卖点之一)不是问题。

摘要

本文探讨了Java语言的本机代码编译问题,重点关注以下主要问题:采用JIT编译器还是静态AOT编译的形式进行动态编译是否更好。

尽管动态编译器在过去十年中已经Swift成熟,到现在为止,各种各样的Java应用程序可以达到或超过通过使用静态编译语言(例如C ++或Fortran)实现所能达到的性能,但动态编译仍然不适用于几种类型的应用程序和执行环境。 尽管AOT编译经常被吹捧为动态编译的弊端的灵丹妙药,但由于Java语言本身的动态特性,在提供本机编译的全部潜力方面面临着一些挑战。

这些技术中的任何一种都不能解决Java执行环境中本机代码编译的所有要求,而是在它们各自最有效的地方使用的工具。 两种技术是互补的。 Runtime systems using both compilation models appropriately will yield benefits to developers and users across a tremendous spectrum of application environments.


翻译自: https://www.ibm.com/developerworks/java/library/j-rtj2/index.html

  • 0
    点赞
  • 1
    收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值