编译字节码与机器码


13

生成临时字节码(例如使用Java)而不是“一路”编写机器代码的编译过程通常不会降低复杂性(因此可能会花费更少的时间)吗?

Answers:


22

是的,编译为Java字节码比编译为机器码更容易。这部分是因为只有一种格式可作为目标(如Mandrill所述,尽管这只会降低编译器的复杂性,而不是编译时间),部分是因为JVM比真正的CPU更简单,编程更方便-因为它是在与Java语言一起,大多数Java操作都以一种非常简单的方式映射到一个字节码操作。另一个非常重要的原因是实际上没有优化发生。几乎所有效率问题都留给了JIT编译器(或整个JVM),因此普通编译器的整个中间部分都消失了。它基本上可以遍历一次AST,并为每个节点生成现成的字节码序列。生成方法表,常量池等会产生一些“管理开销”,但这与LLVM的复杂性相比并没有什么。


您写了“ ...的中间...”。您是说“ ...到...的中间到结尾”吗?还是“ ...的中间部分”?
朱利安·A.

6
@Julian“中间端”是一个真实术语,与“前端”和“后端”类似,无需考虑语义:)

7

编译器只是一个程序,它接收人类可读的1文本文件,并将其转换为机器的二进制指令。如果您退后一步,从理论角度考虑问题,那么复杂度大致相同。但是,在更实际的水平上,字节码编译器更简单。

编译程序必须采取哪些主要步骤?

  1. 扫描,解析和验证源代码。
  2. 将源转换为抽象语法树。
  3. 可选:如果语言规范允许,则处理和改进AST(例如,删除无效代码,重新排序操作,其他优化)
  4. 将AST转换为机器可以理解的某种形式。

两者之间只有两个真正的区别。

  • 通常,具有多个编译单元的程序在编译为机器代码时需要链接,并且通常不与字节码链接。在这个问题的背景下,人们可能会抱怨链接是否是编译的一部分。如果是这样,字节码编译将稍微简单一些。但是,当虚拟机处理许多链接问题时,链接的复杂性会在运行时弥补(请参阅下面的说明)。

  • 字节码编译器往往不会优化得那么多,因为VM可以在运行中更好地做到这一点(JIT编译器是当今对VM的相当标准的添加)。

由此得出的结论是,字节码编译器可以省去大多数优化和所有链接的复杂性,并将这两者都推迟到VM运行时。字节码编译器在实践中更简单,因为它们将许多复杂性带给了机器代码编译器自己承担的VM。

1 不计算深奥的语言


3
忽略优化是很愚蠢的。这些“可选步骤”构成了大多数编译器的大量代码库,复杂性和编译时间。

实际上,这是正确的。我在这里提高学术水平,我更新了答案。

是否有任何语言规范实际上禁止优化?我知道有些语言很难,但不允许任何语言开头吗?
Davidmh 2015年

@Davidmh我不知道有任何禁止他们使用的规范。我的理解是,大多数人都说允许编译器,但不赘述。每种实现方式都是不同的,因为许多优化通常都依赖于CPU,OS和目标体系结构的细节。因此,字节码编译器不太可能进行优化,而是将其下推到知道基础架构的VM。

4

我想说这简化了编译器的设计,因为编译始终是Java到通用虚拟机代码。这也意味着您只需要编译一次代码,它便可以在任何平台上运行(而不必在每台机器上进行编译)。我不确定编译时间是否会减少,因为您可以像考虑标准化计算机一样考虑虚拟机。

另一方面,每台机器都必须加载Java虚拟机,以便它可以解释“字节码”(字节码是Java代码编译产生的虚拟机代码),将其转换为实际的机器码并运行它。

Imo这对于大型程序很有用,但对于小型程序则非常不利(因为虚拟机浪费了内存)。


我知道了。因此,您认为将字节码映射到标准机器(即JVM)的复杂性与将源代码映射到物理机器的复杂性相匹配,而没有理由认为字节码会缩短编译时间吗?
朱利安·A.

我不是那个意思 我说过将Java代码映射为字节代码(即虚拟机汇编程序)将与将源代码(Java)映射为物理机器代码的匹配。
Mandrill

3

编译的复杂性很大程度上取决于源语言和目标语言之间的语义鸿沟以及在弥合这种鸿沟时要应用的优化级别。

例如,将Java源代码编译为JVM字节码是相对简单的,因为Java的核心子集几乎直接映射到JVM字节码的子集。有一些区别:Java有循环但没有循环GOTO,JVM有GOTO但没有循环,Java有泛型,JVM没有,但是可以轻松处理(从循环到条件跳转的转换是微不足道的,类型擦除略少因此,但仍可管理)。还有其他差异,但不太严重。

将Ruby源代码编译为JVM字节代码涉及的工作更多(特别是在Java 7中,invokedynamic并且MethodHandles在Java 7中引入,或更确切地说在JVM规范的第三版中引入)。在Ruby中,可以在运行时替换方法。在JVM上,可以在运行时替换的最小代码单元是一个类,因此必须将Ruby方法编译为不是JVM方法,而是编译为JVM类。Ruby方法分派与JVM方法分派不匹配,在以前invokedynamic,您无法将自己的方法分派机制注入JVM。Ruby具有延续性和协程,但是JVM缺乏实现它们的便利。(JVM的GOTO JVM具有的唯一强大的控制流原语(足以实现连续性)就是异常和实现协程线程,这两个线程都非常繁重,而协程的全部目的是非常轻巧。

OTOH,将Ruby源代码编译为Rubinius字节代码或YARV字节代码再次变得微不足道,因为这两个代码都明确地设计为Ruby的编译目标(尽管Rubinius也已用于其他语言,例如CoffeeScript和Fancy) 。

同样,将x86本机代码编译为JVM字节码也不是简单明了,同样,语义上也存在很大差距。

Haskell是另一个很好的例子:使用Haskell,有几种高性能,具有工业强度的生产就绪型编译器可以生成本机x86机器代码,但是到目前为止,由于语义上的原因,没有适用于JVM或CLI的编译器。差距是如此之大,以至于弥合差距非常复杂。所以,这是一个例子,其中编译为本地机器代码实际上是比编译成JVM或CIL字节码复杂。这是因为本机代码具有较低级别的原语(GOTO,指针等),与使用较高级别的原语(例如方法调用或异常)相比,它们更容易“强制”执行所需的操作。

因此,可以说目标语言的等级越高,它与源语言的语义匹配就越紧密,以降低编译器的复杂性。


0

实际上,当今大多数JVM都是非常复杂的软件,需要进行JIT编译(因此JVM 将字节码动态转换为机器码)。

因此,尽管从Java源代码(或Clojure源代码)到JVM字节代码的编译确实更简单,但JVM本身正在将复杂的转换转换为机器代码。

JVM内部这种JIT转换是动态的,这使JVM可以专注于字节码中最相关的部分。实际上,大多数JVM优化了JVM字节码的最热部分(例如,最调用的方法或执行最多的基本块)。

我不确定JVM + Java与字节码编译器的组合复杂度是否会大大小于提前编译器的复杂度。

还要注意的是最传统的编译器(如GCC锵/ LLVM)正在改变输入C(或C ++,或阿达,...)的源代码为内部表示(GIMPLE为GCC,LLVM为锵),这是非常类似于一些字节码。然后,他们将内部表示形式转换(首先将其内部优化,即,大多数GCC优化过程都将Gimple作为输入并生成Gimple作为输出;然后从中发出汇编程序或机器代码)为目标代码。

顺便说一句,借助最新的GCC(尤其是libgccjit)和LLVM基础结构,您可以使用它们将其他(或您自己的)语言编译为内部Gimple或LLVM表示形式,然后从中端和后端的许多优化功能中获利这些编译器的末端部分。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.