LLVM相比于JVM,有哪些技术优势?

JVM是执行byte code,LLVM是执行IR,如果都是处理中间码,LLVM有什么优势? 感谢Tim Shen的回答,再更新下描述。 假定有这样的场景,我们对一段表达式做代码生成,然后再执行生成的代码。 比较下面两种方案: 1. 生成java的byte code,然后边解释边JIT。 2. 生成LLVM的某种code,然后编译成bin code,再执行。 第一种方案的优点:解释执行一开始还是挺快的,那么响应就比较好,JIT还可以做运行时的优化 第二种方案:编译可能要慢…
关注者
558
被浏览
13583

13 个回答

这问题是个好坑。肯定很多人都对LLVM与JVM的关系有各种误解,包括从业人士也会有片面理解。本来看到这个问题就想马上回答的,被小叶子搅和的根本脱不开身…ToT
还好现在已有的回答都在靠谱的方向上,感觉题主应该已经能充分感受到正解的方向性了。我这里就写点已有答案没写的或者表述不充分的吧。

简单说,LLVM与JVM不是同一范畴的东西,就像苹果和梨,本来不能直接对比。

不过在出于某种特定目的去考察的时候,两者就有一定可比性,例如说同样是要实现一门编程语言,在选择代码生成器(code generator)的解决方案时,是选择LLVM、GCC等,还是JVM、.NET等,还是自己手写,这个角度就可以对它们进行特定的比较。好比想要保健身体而吃水果,是吃苹果更有益还是吃梨更有益,两者可以做特定的比较。

而换个角度,LLVM与JVM并不是互斥的两个技术——两者可以有机的结合在一起。我现在所在的Azul Systems公司就在做基于LLVM的JVM JIT编译器,微软也有一个组在做基于LLVM的.NET JIT/AOT编译器LLILC。好比苹果树和梨树可以嫁接,在梨树上嫁接苹果树得到梨苹果(嗯不是苹果梨)。
正好我们对JVM和LLVM都很熟悉,回答这个问题有更多第一手信息吧。详细请跳传送门:如何看待微软LLILC,一个新的基于LLVM的CoreCLR JIT/CoreRT AOT编译器? - RednaxelaFX 的回答

================================================

一些杂谈

JVM是一套规范,有许多不同的实现,各自取舍差异很大。不指定到非常细致的供应商、目标平台、版本信息的话,根本无法确定说的是怎样的实现。
不要说多个JVM了,就算一个JVM里也可能有多个编译器,要说编译器实现细节的话可得指定清楚具体是哪个了。

LLVM则是单一的实现,虽然可以拆开来有很多花样可玩。只要指定版本和目标平台,还有指定是公版还是带有定制,大家都知道说的是什么。

拿JVM的Java bytecode来跟LLVM IR比的话,这还可以比,因为这都还是在表面跟编译器前端打交道的“接触面”上,定义标准而抽象。但下到下面的实现就必须指定实现来比较了。
目前最流行的JVM实现毫无疑问是OpenJDK / Oracle JDK里的HotSpot VM,正好我也对它最熟悉,所以下面会使用它和若干其它JVM实现来跟LLVM做一下对比。

大家提到“JVM”的时候,大都是把它当作一个黑盒子来用,而不会把它拆开来单独使用其中的一些组件。这个黑盒子包含一篮子运行时服务,典型情况不但有负责执行代码的解释器或JIT编译器,还有GC、线程支持、元数据管理(例如类加载)等功能,外加配套的Java标准库。这些全部打包在一起构成Java运行时环境(Java Runtime Environment),作为一个整体提供给用户使用。
生成Java bytecode就是指望让JVM来运行它,而且通常不会指望JVM把字节码编译出来的机器码吐回给用户做后续的操作——也就是说很少有人会想拿JVM当作静态编译器的后端来用(呃虽说其实这种可能性和相关实验也是存在的…毕竟奇葩)。

而LLVM是一个编译器套件,用法就丰富的多。
最常规的,用它来做静态编译器后端,没问题;
做动态编译器后端,也能行。
单独拿出LLVM的一个或多个pass来做IR到IR的转换也不在话下。
基于LLVM来做调试器,也没问题。
拿LLVM IR当跨平台汇编用也很有趣。
还有许许多多基于LLVM做代码分析的。

================================================

IR层面的对比

之前回答的一个问题正好是预备知识:IR和ByteCode有什么区别? - RednaxelaFX 的回答
JVM的Java bytecode跟LLVM IR都可以看作编译器IR。但它们的设计目的是完全不同的,因而不适合直接比较优劣。

Java bytecode与LLVM IR都用于表述计算的模型,但两者所处的抽象层次不同。
Java bytecode更高层(更抽象),是一种非常高层的IR。其字节码的语义与Java语言的语法结构有非常直接的对应关系,包含大量(类Java的)面向对象语言的高层操作,例如虚方法调用、接口方法调用等。
它最大的“缺点”——没有暴露任何显式的指针操作,因而用于实现一些精密操作时显得拘谨。而且它遵循Java的类型系统,(到Java 9为止)不允许在对象内嵌套对象,在需要精确指定内存布局的场景上无能为力。这些“缺点”都不是一个VM的必然,跟Java字节码相似的.NET的MSIL就有这些自由度。看这个把CoreCLR的JIT编译器单独拆出来插到CPython上的例子,体会一下MSIL字节码的表达能力:Pyjion的代码质量一例 [20160221] - 编程语言与高级语言虚拟机杂谈(仮) - 知乎专栏

LLVM IR更低层(更接近机器),但尚未完全暴露出具体平台相关的特征,所以可以看作一种中层IR。简单的说,C语言能表述的,在LLVM IR里也可以直接表述(没有例外),而C语言所不能表述的,在LLVM IR里多半也不能直接表述(有例外)。

(待续…什么基于栈基于虚拟寄存器之类的表皮差异也可以讨论,还有很多别的更有趣的也可以讨论,例如说用作编程语言实现的code generator时的差异。估计题主关心的也就是这个,但这个展开说可以写太多,一时来不及码字。)

举个小例子来说明Java bytecode与LLVM IR都无法(直接)表述的语义。
假如我们要写一个字节码解释器,想尽可能利用平台相关的资源,特别是想要把解释器栈跟native栈混在一起的话,就会有直接在自己的代码里显式操作栈指针寄存器的需求。在这一点上,Java bytecode的抽象层次太高自然是无法(直接)表述,而LLVM IR也不可以——这就是C语言不能表述的,LLVM IR也无法表述的一种例子。
我们有同事尝试过用LLVM IR来实现跨平台的高性能字节码解释器(希望实现的性能特征跟HotSpot VM的template interpreter类似),但苦于不能方便的在LLVM IR里表述“栈指针寄存器”这个概念,而我们的JVM的解释器调用约定(calling convention)又有点特别,这实验就暂时放下了。中间其实做出过几个实验版是能在x86-64上跑的,但在表述RSP的时候都用了hack(例如说利用intrinsic…),离完美还很遥远。

再举一个小例子来说明LLVM IR能表述,而C语言无法直接表述的语义。
LLVM IR允许对指针指定“地址空间”(address space)。默认的address space 0代表普通内存,而非0的地址空间可以由LLVM的使用者自行指定其语义,例如说显存,又例如说GC管理的内存,等等。最重要的一点是:不同地址空间之间的指针不能混用——这也就隐含了LLVM不会混合优化不同地址空间之间的指针,这是保证LLVM不乱动指针的很重要的手段。

================================================

JVM的JIT / AOT编译器的复杂度

一个JVM的JIT编译器可以容忍多大的复杂度呢?举个例子,前面提到,Azul Systems正在给JVM开发基于LLVM的JIT编译器。这个编译器目前是用类似Clang -O3的pass来把Java字节码编译到机器码的——是的,我们能容忍这样的复杂度,并不是稍微复杂一点的算法就不敢用的。并且它依托于我们的JVM原本就有的profiling和code cache management等基础设施,自带PGO,最终的代码管理也不依赖MCJIT。

即便说回到现有的JIT编译器,HotSpot VM与Zing VM的Server Compiler(C2),其中也使用了许多很重的算法。
它目前最慢的组件是寄存器分配器,使用改进版的Chaitin-Briggs图着色(graph coloring)算法;
它的指令选择用的是一种BURS(bottom-up rewrite system),这个阶段的IR形式跟LLVM的SelectionDAG其实颇相似但比后者更完整一些,是对整个编译单元的;
它有专门做全局调度(global scheduling),当把代码确定调度到某个基本块后,会在基本块内做拓扑排序(以及其它的基于权重的排序)来实现局部调度(local scheduling)。大的全局调度只会做一次,但其实有若干小的全局调度是混在循环优化里做的。要想多加一些调度、排序代码那也是不眨眼的,只要生成的代码质量好;
它会创建许多现代编译器里常见的辅助数据结构,例如dominator tree、loop nesting tree之类,并且会在某些范围内维护这些数据结构的时效性。

而另外一个例子,IBM J9的Testarossa编译器(J9 TR),虽然它最主要的应用场景是在IBM J9 JVM里充当JIT编译器用,但同一个编译器也有用于静态编译COBOL、C/C++等(Static Testarossa,sTR)。在以最高优化层次编译Java字节码时,它能容忍的算法复杂度同样非常高。

这些JVM之所以能容忍JIT编译器使用高复杂度的算法,主要是通过多层编译(tiered compilation)来达到启动开销与顶峰性能之间的平衡。最初代码要么用解释器执行,要么用较低优化程度的编译器或者编译器的较低优化级别来编译,然后逐步把对性能影响大的代码(“热”的代码)用更高优化级别来编译。这样就可以在初期用较低开销达到较高的性能基准,同时并行的慢慢把性能推向峰值。

当然,即便JVM的JIT编译器可以容忍高复杂度的算法,但当其要支持的目标场景的常见情况可以用更低复杂度的算法就达到一样或相似的优化效果时,JVM实现们还是会倾向使用后者的。只是,当大家都把性能推向极限时,堆高复杂度的实现也不是啥稀奇事。

况且,JVM并不是只能有JIT编译器。做AOT编译器也是很自然的事。这些编译器自然可以像一般C/C++编译器那样容忍更高的复杂度。这里就算不展开说。先放传送门把过去回答汇总下:Java中有类似于NGen的工具(AOT编译器)吗? - RednaxelaFX 的回答
大牛们都说得很全了
去就补充一个听来LLVM JIT的应用:
那个Matlab就是先生成自己的IR 再转成LLVM IR
然后让LLVM JIT执行

和mathwork的人闲聊时打听到的 不保证完全正确