如何看待微软LLILC,一个新的基于LLVM的CoreCLR JIT/CoreRT AOT编译器?

软件工程师、主攻高级编程语言虚拟机的设计与实现

244 👍 / 19 💬

问题描述

[LLVMdev] Announcing LLILC: An LLVM based compiler for dotnet CoreCLR.

The LLILC project (we pronounce it "lilac") is a new effort started at Microsoft to produce MSIL code generators based on LLVM and targeting the open source dotnet CoreCLR (github.com/dotnet/corec). We are envisioning using the LLVM infrastructure for a number of scenarios, but our first tool is a Just in Time(JIT) compiler for CoreCLR. This new project is being developed on GitHub and you can check it out at github.com/dotnet/llilc.

* Why a new JIT for CoreCLR?
While the CoreCLR already has a JIT, we saw an opportunity to provide a new code generator that has the potential to run across all the targets and platforms supported by LLVM. To enable this, as part of our project we're modifying an MSIL reader that operates directly against the same common JIT interface as the production JIT (RyuJIT). This new JIT will allow any C# program written for the .NET Core class libraries to run on any platform that CoreCLR can be ported to and that LLVM will target.

* Are we planning to do Ahead of Time compilation?

Yes. The roadmap for the project includes an AOT tool but we're still getting our plans together. There is more detail on this part of the project on the wiki.


项目地址:dotnet/llilc · GitHub
Hacker News讨论帖:news.ycombinator.com/it

这个项目主要是为了让外部CoreCLR参与者能在RyuJIT多有一个选择,特别是便于让有LLVM经验的外部开发者能参与到CoreCLR的开发和移植工作,便于CoreCLR有效的移植到各种平台上。

目前暂时做了JIT,而AOT也在计划中。

微软自己版本的CLR / CoreCLR都还是会用RyuJIT,倒不必因为LLILC的出现而为RyuJIT的前途担忧——除非LLILC的性能把RyuJIT干下去了呵呵…

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

另外,跟主题不直接相关的:
有不少人做了LLVM的C# binding,要用C#写编译器用LLVM后端也很方便。
其中一个:LLVMSharp - C# LLVM bindings generated using ClangSharp

来自问自答了。写个流水账介绍一下在managed runtime中使用LLVM的大背景。

至于具体到LLILC的介绍,以后有机会再补充。

Andy Ayers大大在2015年的EuroLLVM会议上介绍了LLILC,有兴趣的同学可以跳传送门:

The LLVM Compiler Infrastructure Project
LLILC: LLVM as a code generator for the CoreCLR
Andrew Ayers

作为Azul Systems的员工,我们很欢迎微软这个LLILC项目。

2016-07更新:

[八卦] LLILC项目貌似挂了… - 编程语言与高级语言虚拟机杂谈(仮) - 知乎专栏

我们在探索基于LLVM实现适用于我们的Zing JVM的新JIT编译器。为此我们需要给LLVM做一定改造/添加一些新功能来满足我们的需要。但是如果只有我们在推动把这些新功能添加到LLVM主干的话还是略显势单力薄。

LLILC作为CoreCLR的JIT编译器,大背景跟我们完全一致,有合作的空间,当然是好事。LLILC已经明确说了会基于Azul Systems提交到LLVM主干的@llvm.experimental.gc.statepoint intrinsic来实现其准确式GC的支持,这就是个好开始。相信后面还会有更多互动。

GC Support in LLILC

这里说的大背景是说Zing JVM与CoreCLR是:

关于最后一点,后面会举反例说明这是什么意思。我们跟LLILC在最后一点上的取舍可能也不一样:我们是真的尽可能把所有事情都放在LLVM层面做,包括针对Java层面的语义优化;而根据

LLILC Architecture Overview

文档,LLILC以后可能会逐渐在生成LLVM IR之前做更多分析和优化。

关于“准确式”GC与JIT的交互是怎么一回事,请参考我以前一帖:

找出栈上的指针/引用

在Zing VM与CoreCLR之前,使用LLVM的项目里跟这个大背景吻合的一个也没有。其它使用LLVM的managed runtime都有不同的需求。

在managed runtime里使用LLVM,有几种用法:

(回头补充)

下面举几个实际例子。

Mono:跟Zing与CoreCLR情况看似最接近的就是它。Mono有自己实现的JIT编译器,粗略算来可以看作有两大版本:老版本实在太简单,效果不够好;后来新写了一个版本,用上了不少时髦有效的JIT编译优化,效果还不错,但顶峰性能还是不够好。所以,在能够容忍更耗资源的JIT及AOT编译的场景里,Mono就开始尝试用LLVM做后端了。

Mono以前用Boehms GC,默认是完全保守式的mark-sweep GC,不移动对象;也可选用半保守式——栈和寄存器的扫描还是保守扫描。较新版本的Mono采用自己新实现的SGen GC,默认使用准确式GC,但一定条件下还是会使用半保守式。

当使用LLVM作为JIT或AOT编译器时,Mono会自动选择使用半保守式的SGen。这个行为可以从

mini_gc_init_gc_map()

看出来。

所以Mono选择偷懒避开LLVM与准确式GC的交互问题。还有些别的问题,这里不展开说了。

ART:Google的Android Runtime(ART)曾经尝试用LLVM实现其AOT编译器的其中一个后端,“Portable”。

它虽然让LLVM与准确式GC搭配使用,但采用了很绕弯的方式,自己实现了shadow stack frame嵌入到LLVM IR中,然后在每个call指令前都把live variable存到shadow stack frame里,等call之后再取回出来。这等于所有live variable在call site前都要被spill,性能会比较糟糕。这原理上跟@llvm.gcroot机制很像,但ART里的Portable后端并没有使用这个机制。

当然,后来Google干脆把Portable后端连同LLVM一起从ART里删除了,也就没LLVM啥事了。请参考另一个回答:

Android 中的 LLVM 主要做什么? - RednaxelaFX 的回答

提到ART与LLVM,想多说几句:LLVM不是万能药,不是说用了LLVM就自然有AOT编译的能力了。LLVM只是一个compiler infrastructure,实现AOT的许多麻烦地方还是得靠自己解决。实在太多不明就里的人看到ART的代码里有用到LLVM就把它hype上天了,这个完全没必要嗯…

VMKit:VMKit一直在用LLVM做JIT编译器。

在转用MMTk实现GC之后,VMKit倒是开始用准确式GC了。它依赖LLVM现有的@llvm.gcroot机制来注册GC root。但这个机制有诸多问题,下面再说。请参考

Precise and Efficient Garbage Collection in VMKit with MMTk

JavaScriptCore/FTL:Apple给JavaScriptCore猛打鸡血,从SquirrelFish抛弃AST解释器改用字节码解释器,到SquirrelFish Extreme(SFX)进一步把字节码解释器改造为“名为context threading解释器”的超简易JIT编译器,到抛弃这个解释器和简易JIT编译器,逐步改为用 LLInt解释器 + Baseline JIT简易JIT编译器 + DFG优化JIT编译器的强力正统组合,性能一路狂赶,总算能声称自己比V8不相上下甚至有所超越了。

但Apple还没满足,决定在DFG的基础上再加一层更优化的编译层,叫做Fourth-Tier LLVM(FTL)。

JavaScriptCore坚定的使用Bartlett-style GC。它虽然会移动对象,但栈与寄存器的扫描却是保守式的,所以还是避开了让LLVM与准确式GC交互的麻烦。FTL组里的某些个同志坚定(是超级坚定)的相信Bartlett-style GC是宇宙正解,用完全准确式GC的都是傻逼;这个咱就懒得争论了。

(那位同志…其实把所有跟他观点不同的人都看作傻逼,实在无法交流)

GC方面FTL虽然偷懒了,但为了支持FTL,Apple还是花了些精力给LLVM添加

@llvm.experimental.stackmap 和 @llvm.experimental.patchpoint.*

intrinsics,前者的主要使用场景是deoptimization / OSR exit,后者的则是inline caching。这些对动态优化,特别是对动态类型语言的优化有很大用处。要注意的是这俩intrinsic函数所用到的stack map,跟准确式GC所需要的GC stack map有所不同:前者主要考虑的是捕获所有live variable的信息以便deoptimization使用;而后者只关心live managed pointer,不需要所有live variable。

然而FTL与Zing VM或CoreCLR在使用LLVM的方式上还有更深入的差别:

FTL真的是只把LLVM用作编译器后端,而前端其实还是接在原本的优化编译器DFG上的。所有跟JavaScript的动态类型语义相关的优化都在DFG里做完了。从优化过的DFG IR生成出来的LLVM IR,其实已经接近从C语言编译生成的形态,动态语义被尽可能的抹去了。这样,DFG做了它擅长的事情——按JavaScript语义做特定优化,而LLVM也可以做它最擅长做的事情:对跟C语言语义相似的代码做优化并生成机器码。这种组合从执行效率来说是很好的,而且也很合理——DFG已经在那里了,干嘛不用;但从复用LLVM的整个编译流程来说就不够深入了——即便FTL能优化编译JavaScript代码,别的动态语言的编译器还是得自己实现一遍自己语言的动态语义优化才可以用同样的方式使用LLVM。

Zing VM与CoreCLR都更希望深入的复用LLVM的编译流程,尽可能将Java或C#的语义优化融入到LLVM里,而不是像FTL那样有个DFG前端来做语义优化。

关于FTL,请参考下面两篇博文:

LLVM Project Blog: FTL: WebKit’s LLVM based JITIntroducing the WebKit FTL JIT - Surfin' Safari

2016-02更新:结果Filip大大2015年10月就谋划新写一个编译器后端来替换FTL中的LLVM,然后到2016年2月正式对外宣布了新的B3。请跳传送门:

[新闻][JavaScript引擎] WebKit JavaScriptCore用新的B3编译器后端替代FTL JIT中的LLVM - 编程语言与高级语言虚拟机杂谈(仮) - 知乎专栏

Unladen Swallow:Google两位同行做的小项目,想给CPython安上LLVM做JIT编译器。项目是死了,不过还是可以看看当时他们做了些啥。

Unladen Swallow的runtime直接源自CPython。这个决定为后面项目的进展推进缓慢埋下了伏笔。想要把一个从来没考虑过为JIT编译器优化的runtime改造成适合搭配JIT编译器使用,需要很大很大的努力。JavaScriptCore是一个改造成功的例子;PHPNG能否成功还有待观察。但Unladen Swallow无疑是个失败的例子。

Unladen Swallow的梦想很宏大:扔掉GIL,扔掉引用计数改为tracing GC,加上JIT编译器并且做动态类型相关优化。诶。设想中的GC想做成准确式GC,利用LLVM的@llvm.gcroot机制来注册GC root。

到最后,Unladen Swallow还是没有把设想中的GC做好。Unladen Swallow还是在用原本CPython的引用计数。

而且优化也没怎么做,基本上就是把CPython的解释器每条字节码的处理函数翻译成了LLVM IR而已;然而很多动态类型相关的操作细节都还封装在runtime里,导致LLVM也做不了多少优化。Unladen Swallow做法的效果恐怕比JavaScriptCore在SFX时代的做法好不了多少,实在悲催。

IBM的Fiorano JIT项目的“失败”在技术上感觉跟Unladen Swallow有非常相似的地方——两者都是想在CPython的runtime的基础上、添加一个基于现成的编译器框架的JIT编译器,只不过前者基于Testarossa编译器,后者基于LLVM。在runtime层面上遇到的困难都很相似。

从Fiorano的论文的结论看,感觉对这些动态类型语言而言,像JavaScriptCore/FTL那样有个做语言层面优化的DFG前端会好很多,而且runtime自身的缺陷带来的性能损失也很显著,必须配套改进才有效果。

Rubinius:Rubinius的JIT编译器也使用LLVM,而且有准确式、会移动对象的、分代式GC。

Rubinius对LLVM的用法其实跟Unladen Swallow在差不多的层面上,前者比后者稍微好一些;毕竟后者是参考前者的嘛。虽然做的优化不多,Rubinius好歹把东西都做出来了。

Rubinius在生成LLVM IR的时候没做多少针对动态类型的静态分析,大部分call site还是靠inline caching这种动态行为来实现。

Rubinius生成LLVM IR的过程中并没有通过抽象解释把局部变量变为SSA scalar,而是继续保持解释器的栈帧布局,把局部变量和临时变量放在位于内存的栈帧里。这样,跟准确式GC交互的问题就“自然”解决了——只要跟解释器共用一套stack map机制就行。但这样显然极大的牺牲了优化,要把性能推向极限的话肯定不能这么搞。

我觉得除了参与者更多、更有爱,资源更充足(有Engine Yard赞助)之外,很重要的一点就是Rubinius不是直接拿CRuby(MRI)来改造,不然难说会不会跟Unladen Swallow一个命运。

别的项目的例子就先说到这里吧。还有很多有趣的项目但是无法一一列举了。

前面提到

@llvm.gcroot

有缺陷,一方面它会干扰LLVM里的各种优化,基本上LLVM的内建优化遇到gcroot就跳过不优化了;另一方面它的语义不够清晰,使得即便用了它还是有可能用错。同事Philips Reames写了篇很好的介绍文讲解@llvm.gcroot的问题:

Why not use gcroot?

而为了解决@llvm.gcroot的弊病,我的同事们新实现了前面提到过的@llvm.experimental.gc.statepoint系列intrinsic函数,以及在优化向IR插入statepoint的“late statepoint insertion”功能。前者以及在LLVM主干,后者我不清楚现状如何。

言归正传:越多高质量的、大背景相同的项目参与到LLVM中,对我们来说就越好。所以我们很高兴看到微软为CoreCLR制作了LLILC。

带这个项目的Andy Ayers以前带过微软的Phoenix Compiler Infrastructure项目,是个靠谱的大牛。

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

当然,除了Apple自己人之外的人在实际项目中用LLVM都会遇到不少蛋疼的事情。

我们的基于LLVM的编译器,依赖的LLVM是svn repo的最新版,时常保持同步。这也就意味着经常在同步了LLVM代码后我们的编译器就有些地方不对了,然后得手动改。

时常保持更新LLVM代码的好处是,就算一次更新break了我们的编译器,要做的修改还可控;如果是依赖某个LLVM release,直到下次release才再次更新的话,那就…呵呵了。

以后想起啥再写点。写到这里都还没真写啥,居然都已经达到老赵

@赵劼

的“太长不看”长度了orz