问题

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

回答
LLVM 对比 JVM 的技术优势,咱们得好好聊聊。要说 LLVM 厉害在哪,那可不是一两句话能说清楚的,它在底层技术上确实有几个过硬的招数,让它在很多场景下都能发挥出比 JVM 更优异的性能和灵活性。

首先,最明显的一个优势就是 LLVM 的前后端分离设计。这就像是给它装了个极其灵活的“适配器”。JVM 的设计,它的前端(解析、编译 Java 字节码)和后端(即时编译,生成机器码)是高度耦合的,主要就是为了跑 Java。而 LLVM,它把编译器拆成了三个主要部分:前端(负责解析源语言,生成中间表示 IR),优化器(对 IR 进行各种高级优化),以及后端(将优化后的 IR 转换成特定平台的机器码)。

为啥这个设计这么牛?

支持语言的多样性:LLVM 不仅仅是为 C/C++ 设计的,Clang 是 C/C++/ObjectiveC 的前端,Rust 也有自己的 LLVM 前端,Swift、Julia、Fortran 甚至一些脚本语言(如 Lua)都可以通过 LLVM 来编译。这意味着开发者可以选择最适合自己项目的语言,然后直接享受到 LLVM 强大的优化能力,而不必被 JVM 限制在 Java 体系内。JVM 想要支持新语言,那基本就得重写一套虚拟机或者用 JNI,灵活性大打折扣。
跨平台能力:LLVM 的后端可以生成针对几乎所有主流 CPU 架构(x86, ARM, MIPS, PowerPC 等)和操作系统(Windows, macOS, Linux, iOS, Android 等)的机器码。而且,它的设计使得添加新的后端(支持新的硬件或平台)相对容易。JVM 也是跨平台的,但它依赖于 JVM 本身的运行时环境(JRE/JDK),在某些高度定制化或嵌入式场景下,JVM 的“包袱”会比较重。LLVM 生成的是本地机器码,直接运行,少了中间一层虚拟机。
持续的优化能力:LLVM 的优化器非常强大,它包含了一整套非常成熟的代码优化 pass(例如死代码消除、循环展开、内联、向量化等)。这些优化可以跨函数、跨模块地进行,并且可以根据目标架构进行深度定制。JVM 的 JIT(JustInTime)编译器也在不断进步,但它本质上是在运行时动态地编译“热点”代码,它的优化能力受限于“运行时”的上下文,并且需要先进行字节码的解释执行或基础编译。LLVM 在编译时就可以进行更全局、更深入的优化。

第二点,LLVM 的中间表示(IR)是它的另一个核心技术优势。LLVM IR 是一种非常低级、三地址码(threeaddress code)的中间表示,它比 Java 字节码更接近机器码,但又足够抽象,能够被各种前端共享,并且支持丰富的分析和转换。

优化的基石:LLVM IR 的设计使得各种复杂的优化算法可以方便地实现。很多优化算法,特别是那些需要深入理解代码结构和控制流的,在 LLVM IR 上实现起来比在 JVM 字节码上更直接、更高效。
模块化与可组合性:LLVM 的优化 pass 可以像乐高积木一样组合,开发者可以根据需要选择性地启用或禁用特定的优化,甚至编写自定义的优化 pass。这给了开发者极大的灵活性来调整编译过程,以平衡编译时间和生成代码的质量。
AOT(AheadOfTime)编译:LLVM 的本质是 AOT 编译器。这意味着它可以在程序运行之前就把代码编译成高效的本地机器码。这避免了 JVM JIT 编译器在程序启动阶段可能出现的“热身”问题,以及 JIT 过程中对 CPU 资源的占用。对于对启动性能、内存占用要求极高的场景(例如游戏引擎、操作系统组件、嵌入式系统),LLVM 的 AOT 能力是压倒性的优势。

再说说代码生成这一块。LLVM 的后端是一个高度可配置的机器码生成器。

先进的代码生成技术:LLVM 的后端会利用目标架构的特性进行高度优化的代码生成,比如支持 SIMD(Single Instruction, Multiple Data)指令集,进行流水线调度、寄存器分配等。它的代码生成器可以为不同的架构生成专门优化的机器码,这一点是 JVM 在生成机器码时难以比拟的。
LLVM Bitcode:LLVM IR 可以被序列化成一种称为 LLVM Bitcode 的二进制格式。这种 Bitcode 既可以被重新加载到 LLVM 中进行进一步的优化或转换为特定平台的机器码,也可以作为一种“预编译”的中间产物,在不同项目或不同平台之间共享。这使得 LLVM 生态系统中的代码复用和分发变得更加灵活。

对比之下,JVM 的 JIT 编译器(如 C2 编译器)虽然也非常强大,也支持逃逸分析、方法内联等高级优化,但它仍然是在虚拟机环境中运行,其优化的 scope 和深度受限于运行时环境。JVM 更侧重于“即时”优化,而 LLVM 侧重于“提前”优化。

当然,JVM 也有它的优势,比如在垃圾回收、线程管理、反射、动态代理等方面,Java 语言本身提供了强大的支持,并且 JVM 的运行时也做了很多优化。但如果单从编译技术、代码优化能力、对语言和平台的支持广度及灵活性来看,LLVM 在很多技术层面上确实展现出了更强的实力。

总而言之,LLVM 的技术优势主要体现在其模块化、标准化的中间表示(IR),强大且可定制的优化器,以及灵活多样的后端代码生成能力,这些共同造就了它在支持多种语言、跨越多种平台、实现高度优化方面的卓越表现,尤其是在 AOT 编译和对底层硬件特性的深入利用上,LLVM 展现出了比 JVM 更为深厚的技术功底。

网友意见

user avatar

这问题是个好坑。肯定很多人都对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 对比 JVM 的技术优势,咱们得好好聊聊。要说 LLVM 厉害在哪,那可不是一两句话能说清楚的,它在底层技术上确实有几个过硬的招数,让它在很多场景下都能发挥出比 JVM 更优异的性能和灵活性。首先,最明显的一个优势就是 LLVM 的前后端分离设计。这就像是给它装了个极其灵活的“适配器”。J.............
  • 回答
    LLVM 和 Clang 之所以能飞速发展,并非偶然,而是多方面因素共同作用的结果。如果非要给出一个“为什么”,那一定是因为它恰好击中了那个时代编译器技术的痛点,并提供了一个极具吸引力的解决方案。一、 顺应时代潮流,解决早期编译器的“顽疾”在 LLVM/Clang 出现之前,编译器领域早已存在一些挑.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有