软件的变革与 AOT
前言
AOT 即 Ahead of Time Compilation,即运行前编,与之对应的是 JIT。众所周知,程序的源码并不能够被处理器直接执行,编程语言基本上都是人类可读,编译器或者解释器就需要将源代码转变为 CPU 可以操作的指令。比如一个加法函数最终需要执行 addl 汇编指令对应的机器码。
add.c
int add(int x,int y){
return x+y;
}
clang -S add.c
汇编文件 add.s
.text
.def add;
.scl 2;
.type 32;
.endef
.globl add
.align 16, 0x90
add: # @add
.Ltmp0:
.seh_proc add
# BB#0:
pushq %rax
.Ltmp1:
.seh_stackalloc 8
.Ltmp2:
.seh_endprologue
movl %ecx, 4(%rsp)
movl %edx, (%rsp)
movl 4(%rsp), %ecx
addl %ecx, %edx
movl %edx, %eax
popq %rdx
retq
.Ltmp3:
.seh_endproc
对于转变为机器码的时机,不同的语言有着不同的选择,或是完全转变为机器码后运行,或是在运行时转变为机器码。AOT 便是运行前转为机器码。 ,事实上C/C++ D,Pascal,Fortran之类的语言本质上也是 AOT,但本文讨论的 AOT 主要针对的是对于 Java,NET 等框架或语言的 AOT。
以 Java 为例,Java 源码被编译器 Java Vitrual Machine ByteCode,当需要执行的时候,将 JVM 指令一条一条的转变为对应处理器的指令,后执行,(实际上x86 上模拟执行 ARM 架构的程序也可以是这个套路。)但是这个效率并不高,而且不好优化,而 JIT 的做法是将字节码编译成对于处理器的指令后运行。这比纯解释又快了许多。
LLVM 编译器基础设施的发迹
数年前,LLVM 的官网对于 LLVM 项目的介绍是: “Low Level Virtual Machine”,低级虚拟机,而现在对 LLVM 的介绍是:The LLVM Compiler Infrastructure,即编译器基础设施。 在程序员圈子中对 LLVM 最深刻的影响来自于 Clang,C 家族编译器(C/C++ /Objective-C/C++ Compiler)前端,Clang 是 LLVM 最成功的实现,在平台支持上,Clang 短短几年达到了 GNU C Compliton (GCC) 20年的高度。 Clang 在编译速度,占用内存,以及整个框架的设计上都是可圈可点的,对用户友好的开源许可证 The University of Illinois/NCSA Open Source License (NCSA). 实际上就有商业编译器依赖Clang实现,比如:Embarcadero™ C++ Builder 的 Win64 编译器 bcc64 就是完全基于 Clang 实现(3.1 trunk)。而 C++ Builder 前身是 Borland C/C++&Turbo C.
下面bcc64的命令实例:
bcc64 -cc1 -D_RTLDLL -fborland-extensions -triple=x86_64-pc-win32-elf -emit-obj -std=c++11 -o Hello.o Hello.cpp
看过《C/C++圣战》 大抵也知道 Borland C/C++ 曾经是多么的辉煌,而现在却选择了 Clang 来实现 Win64 工具链 (C++ Builder 10 32位也使用了 clang)。
一方面,单从 C 语言家族来讲 Clang 基于库的模块化设计,易于 IDE 集成及其他用途的重用。比如 Sublime Text,VIM,Emacs 都有基于 Clang 实现 C/C++ 代码自动补全,Clang 提供一个 libclang 的库,可以编译成动态也可以编译成静态库,SublimeText 的 C/C++ 插件 SublimeClang 就是使用 libclang.dll(so/dylib)。其他的编译器对于 IDE 集成的支持是远远不及的,比如 Visual Studio IDE 对于 C++ 的智能提示是使用 EDG C++ Frontend 目前 Clang 在 C++ 的标准上,远远优于其他主流编译器 Microsoft C++(cl),GCC (g++)。
另一方面,LLVM 实现了一套可扩展的编译器实现方案,任何人需要实现一个语言,只需要实现一个前段,然后将源码编译成 LLVM 字节码,也就是 LLVM IR, 然后 LLVM llc 将源码编译成不同平台的机器码,并且优化。比如最近正火的语言 Rust 后端也使用了 LLVM,以及 D 语言编译器 ldc,Go 语言编译器 llgo 等等。而 LLVM 不仅仅拥有 AOT 的能力,而且还有 JIT 模块, LLVM ExecutionEngine ExecutionEngine 的 API 并不是非常稳定。
传统的编译器
传统编译器需要经过前端(Frontend),优化(Optimizer),后端(Backend)然后将源代码转变为机器码。
Three Major Components of a Three-Phase Compiler
如果需要增加一种新的平台的支持,这种模型无法提供更多的可重用的代码。
要添加其他语言的支持模型如下:
Retargetablity
基于 LLVM 的编译器
基于 LLVM 的编译器架构如下:
LLVM’s Implementation of the Three-Phase Design
基于 LLVM 的编译器前端将源码编译成 LLVM IR,然后在使用优化编译器编译成对应平台的机器码,一个很鲜明的对比是 D语言的编译器 DMD 与 ldc,DMD 是传统的编译器,而 ldc 是基于 LLVM 的编译器,DMD 目前依然只支持 x86/x86_64 架构处理器,而 ldc 可以生成 ARM64,PPC,PPC64, mips64 架构的机器码。详细的介绍可查看: Dlang Compilers
LLVM IR 可以反汇编成人类可读的形式,LLVM IR 类似于 RSIC 指令。
clang add.c -S -emit-llvm
add.ll
; ModuleID = 'add.bc'
target datalayout = "e-m:w-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-w64-windows-gnu"
; Function Attrs: nounwind uwtable
define i32 @add(i32 %y, i32 %x) #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
store i32 %y, i32* %1, align 4
store i32 %x, i32* %2, align 4
%3 = load i32, i32* %2, align 4
%4 = load i32, i32* %1, align 4
%5 = add nsw i32 %3, %4
ret i32 %5
}
attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"clang version 3.7.0 (tags/RELEASE_370/final)"}
使用以下命令即可:
clang add.ll -c
也可以使用 llc 命令编译
于2010年Chris Lattner 被 ACM 授予 “Programming Languages Software Award” 。2014年 Chris Lattner 作为苹果编译器开发团队的首席架构师,在 Apple WWDC 2014 推出了Swift。而 Swift 就是基于 LLVM 的,使用如下命令编译 swift 代码,即可得到 LLVM IR 代码。
swiftc -S -emit-object hello.swift
通过 LLVM 在 Android 上运行 Swift 代码
整个 LLVM 项目推出了很多重量级工具,除了 Clang 之外,还有 LLVM 调试器 lldb, LLVM 连接器 lld,目前都可以在 Windows, Linux ,Mac ,以及 BSD 上运行,目前 XCode 自带有这些工具,Windows 上,clang lld 都是能够安装集成到 Visual Studio 的。
很多公司贡献了代码到 LLVM 项目中,或使用 LLVM 的工具改善自己的产品,比如 Google ,Google NDK 以及 PNacl 都使用了 LLVM 的工具,而 LLVM 的许多特性就是 Google 实现的,比如地址消毒剂 AddressSanitizer(GCC 目前也支持了)。还有 Intel OpenCL, Adobe, NVIDIA Nucda,Microsoft WinObjc。
Android 与 AOT
LLVM 优异的架构并没有被 Android 广泛使用。Android 最初由 Andy Rubin 开发作为数码相机的操作系统,使用 Linux 内核,后来发现市场需求不大被改造成智能手机操作系统反而获得了巨大成功。Rubin 选择了具有很大争议的 Java 作为 Android 的应用开发语言,Java 基于 JVM,能够在支持 JVM 的平台上运行,Java 的开发者非常多,你可以在中国任何一个理工科大学找到学习 Java 的学生,漫天遍地的 Java 培训机构,这对于 Android 来说非常有利,从 Google 收购 Android开始,这一切已然水到渠成。Android 使用的是 Dalvik 的虚拟机,这与 Java 官方的 JVM 技术上稍微有些差异,JVM 是一种堆栈机器,而 Dalvik 是寄存器机,孰优孰劣,也不太好评价,正如 CISC 与 RSIC 的争议,实际上对于软件而言,架构,编码实现,编译器(解析器),都会给软件的性能带来巨大的影响,时常发现某某 JavaScript 升级换代,性能增加一倍。
Android Runtime
2014年6月,Google 推出 Android 5.0(Android Lollipop) ,ART 完全取代了 Dalvik。
ART 本质上一个混合的 AOT 方案,它还实现了 JVM 解释器。
Andy Rubin 先后在苹果 微软 谷歌公司工作过。
.NET 与 AOT
说起.NET 就不得不谈到 Anders Hejlsberg 此人,他来自丹麦,Turbo Pascal 最开始就是他开发的,Delphi/C#之父,C#&.NET 的首席架构师,TypeScript 的首席架构师,主持开发了 .NET Framework,Visual Basic.NET,以及最新的 .NET 编译器 Roslyn 。
值得注意的是 TypeScript 完全基于 ECMAScript 6标准草案开发,Java 的流行以至于微软也坐不住,在上个世纪末,微软也开发了自己的Java虚拟机,最初微软推出的是Visual J++,而在Anders加入微软后立即被委以重任,Visual J++在性能上甚至超越了Sun JVM,这个Sun带来了恐慌,Sun 以破坏兼容性将微软告上公堂,微软最终放弃了Java的开发,而C#与.NET也诞生了,.NET在设计上确实借鉴了Java的很多理念,并且超越了Java,这也是 Anders 从 Borland 就存在心中的构想。
类似于 LLVM 的研究,微软很早就有,这个项目是:
Phoenix Compiler and Shared Source Common Language Infrastructure
现在的 Microsoft Visual C++ 就有 Phoenix 编译器架构的技术积累。
Chris Lattner 曾于2004年在微软研究院实习,参与微软的 Phoenix Compiler Framework 项目,或许对于微软来说,应该感到遗憾,Chris Lattner 并没有最终加入微软,而是加入了苹果公司。很多时候技术是相互影响的,好的技术最后都会殊途同归。
在我刚进入大学的时候,刚刚学会编程,曾经下载过08版的 Phoenix Compiler 编译器工具,并且也试用过,不过到现在已经无法下载了。而 Phoenix Compiler Framework与LLVM的理念确实很相似,并且可以得知的是,Phoenix 很多的技术被整合到微软的 Microsoft C/C++ Compiler,就技术上而言 Phoenix 与 LLVM 有许多相似之处,比如都能转变成 IR,拥有软件优化和分析框架,然而具体的中间语言是不一样的。
Phoenix 的架构师 Andy Ayers 本人也是 LLILC 的核心成员。
Phoenix不仅仅限于一个编译器,它还是一个软件优化和分析框架,能被其他编译器和工具使用。 它能生成二进制代码,也能输出MSIL程序集。源代码可以经过分析, 并被表示为 IR(中间表示,Intermediate Representation)形式,这种形式可以在后期被各种工具分析和处理。
—-InfoQ: Phoenix编译器框架说明
在 .NET 未开源时,微软研究院还提供了一个 .NET 的学习代码 “Shared Source Common Language Infrastructure”的源代码下载。
为什么说些无关的东西?实际上,微软的 .NET Native 实现离不开 Phoenix 编译器的技术研究。
.NET Framework 三阶段图:
.Net Three-Phase
.NET Framewok Native & JIT 模型
.NET Compiler Platform (“Roslyn”)
Roslyn 是 Microsoft 推出的新一代 C#/VB.NET 编译器,相对于传统的 .NET C# 编译器,整个生产流程结构非常清晰,和 C++ 中的 clang 类比丝毫不为过,而 Visual Studio 2015 也充分利用了 Roslyn 的优秀特性.
目前无论是 Microsoft 还是 Mono 都参与到了 Roslyn 的开发过程中,利用 Roslyn ,一些第三方的 C# AOT 解决方案迅速的发展起来.
编译器管道:
编译器管道及对应的 API:
编译器 API 和 服务:
Roslyn APIs:
.NET Native
.NET 的 AOT 解决方案在 Mono 中很早就出现了,Mono 平台支持 Android 以及 iOS 的 App 开发,由于 iOS 禁止第三方软件的 JIT 编译,在iOS 平台,Mono 使用的就是 Full AOT 策略.
Program.cs
using System;
namespace hello
{
class MainClass
{
public static void Main (string[] args)
{
Console.WriteLine ("Hello World!");
}
}
}
使用 Mono 编译:
mcs Program.cs
然后使用 mono AOT 编译成机器码:
mono –aot=full,nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048 Program.exe
使用 objdump 反汇编:
objdump -d Program.exe.so >Program.s
这里只反汇编了执行段,Program.s:
.NET Framework 一直有一个工具, NGEN (Native Image Generator), NGEN 会将程序集简单的编译成机器码,在C:\Windows\Microsoft.Net\assembly 目录就是 NGEN 的镜像. NGEN 依然无法脱离 .NET Framework,任然需要 JIT,程序运行的时候往往是 MSIL 和 MachineCode 混合运行.
Windows update 更新重启后,经常可以在任务管理器里面发现 NGEN 进程疯狂的执行任务.
在没有 .NET Native 时, Windows Phone 中,.NET App 在安装后就会通过 NGEN 转变为机器码,以此来提升运行速度,降低功耗.对于 .NET Native 的需求,随着 Microsoft 的 移动战略的实施变得尤为迫切.
早在2013年就有传闻,.NET将推出.NET Native,时至今日,基于 .NET 的 Windows 10 通用应用程序,都开始开启 .NET Native 支持.
.NET Native 基本的流程如下:
App IL + FX -> MCG -> Interop.g.cs -> CSC -> Interop.dll -> Merge -> IL transform -> NUTC -> RhBind -> .EXE
.NET Native 工具链将所有依赖到的程序集反汇编成 C# 源码,使用 C# 编译器再编译成一个 dll, dll 再转 IR ,使用 nutc_driver
编译成机器码, 而 nutc_driver
代码是使用了 Microsoft C++ 后端代码. 最后生成一个 dll 和一个 Bootstrap 的 EXE, dll 导出的函数为:
RHBinder__ShimExeMain
使用 Visual C++ 工具 dumpbin 查看符号信息:
dumpbin /EXPORTS App2.dll
得到的结果如下:
查看 App2.exe 导入的符号信息
dumpbin /IMPORTS App2.exe
输出如下:
.NET Native 的实现,在 IR 前期很大的程度上依赖 Roslyn 这类新型的编译器,而在 IR 后期,就得益于 Phoenix 编译器框架, .NET Native 后端和 Visual C/C++ 共用一套后端优化编译器。
在 Microsoft Channel 9 有一个对 .NET Native 的介绍视频: .NET Native Deep Dive
视频中的 PPT 可以下载: .NET Native PPTX
在 Visual Studio 2015 中,可以使用 NuGet 安装 .NET Native 的相关插件,以此来分析 .NET 引用能否被 .NET Native 支持。
Install-Package Microsoft.NETNative.Analyzer
对于 .NET Native, 大多数人并不会感到满意,大多数 .NET 开发者都希望 .NET Native 能够扩展到 桌面平台,能够支持 WPF …
LLILC - LLVM-Based Compiler for .NET CoreCLR
在 .NET CoreCLR 开源后,.NET 开发团队也创建了基于 LLVM 的 .NET Core 编译器项目 LLILC,实际上,在之前已经有了 C# Native, SharpLang 之类的项目着手实现 .NET 的 AOT。然而这些项目大多是个人兴趣,支持有限。
LLILC 的核心开发者是 Phoenix 编译器框架的架构师 Andy Ayers, 大神本人也会在 gitter.im 上回答人们对 LLILC 的疑问。LLILC 包括 JIT 和 AOT ,不过目前 AOT 并没有编码实现。目前项目组的重心任然是 JIT 模块。
LLILC 的 JIT 架构
LLILC 的 AOT 架构
MRT 也就是 .NET Native Runtime ,专门为 .NET Native 实现的一个精简运行时。
LLILC 依然是非常的不完善,最后的究竟怎样仍需观望。
从 .NET 还是 JVM 或者是 LLVM 来看,很多东西都是相似的,技术也在互相影响和渗透。
.NET Core Runtime (CoreRT)
近期,.NET 推出了 .NET Core Runtime (CoreRT) 的项目,此项目和 .NET Core Runtime (CLR) 不同的是,CoreRT 提供了一套 .NET AOT 的机制,可以将 .NET 程序 编译成原生代码,不依赖 .NET 运行时而运行在宿主机器上。此项目的大部分代码来源于 CoreCLR ,也有部分与 UWA .NET Native 的代码类似。
这种 AOT 的优化的好处,文档中也有介绍
- 编译后生成一个单文件,所有的依赖,包括 CoreRT
- 启动时是机器码,不需要生成机器码,也不要加载 JIT 编译器
- 可以使用其他优化编译器,包括 LLILC ,IL to CPP
目前支持的是 Console App, 计划支持 ASP.NET 。
CoreRT 有两个方式生成机器码,第一个 使用是直接编译 IL 成机器码,默认情况下, RyuJIT 作为一个 AOT 编译器将 IL 编译成机器码, 实际上这是一个很巧妙的策略,在 CoreCLR 中, RyuJIT 又变成了一个简单的 JIT 编译器。在前文中提到的 LLILC ,也可以作为 CoreRT 的 AOT 编译器。
另一个方式是将 C# 代码编译成 C++ 代码,然后调用对应平台的 C++ 编译器优化编译成机器码。
在 CoreRT 介绍文档中, 提到了 UTC for UWP apps 也可以作为 CoreRT 的 AOT 编译器
项目地址: .NET Core Runtime
Channel9 视频: Introducing .NET Core: A Cross-Platform Runtime
CoreRT 介绍: Intro to .NET Native and CoreRT
目前,在 dotnet.github.io 页面可以获取 Windows , Ubuntu, 以及 Mac OS X 的安装包,这个是每日构建的。
初始化一个 dotnet 项目
dotnet init
安装依赖
dotnet restore
直接使用 RyuJIT 编译成机器码:
dotnet compile –native
编译生成 C++ 代码:
dotnet compile –native –cpp
实际上此项目还相当不完善,dotnet 工具链偶尔是无法运行的,不过可以预见此项目会给人们带来眼前一亮的感觉。
探索的脚步
4.1 CSNative
永远不会有完全统一的意见,总会有人去创造新的轮子。不谈其他,重复的创造能对已有的东西带来技术革新,在 CodePlex上,就有个伙计实现了自己的 .NET Native 方案:C# Native;他利用 Roslyn API 将 C# 编译成 MSIL,然后将 MSIL 编译成LLVM IR ,随后 ‘LLVM System compiler’ llc 编译成 Native code ,用 GCC 将 Object 文件链接成 exe,GC 库是32位的 libgc, 现在已经转变了策略,直接生成 C++ 代码,使用 G++ 编译成二进制。
Il2c 是一个利用 Roslyn 实现的 C#/MSIL to C++ 的编译器
Il2c.exe helloworld.cs /corelib:CoreLib.dll
生成 helloworld.cpp, 然后使用 g++ 编译成 exe :
g++ -o helloworld.exe helloworld.cpp CoreLib.cpp -lstdc++ -lgcmt-lib -march=i686 -L .
直接生成 Exe:
Il2c.exe /exe helloworld.cs /corelib:CoreLib.dll
C# Native 作者 AlexDev 本人也是 Babylon 3D (C#/native port) 的作者。
4.2 SharpLang
同样的,在 Github上,也有一个基于 LLVM 的 C# Native 的解决方案: SharpLang。 在LLILC推出后,开发者 Virgile Bello 也就没有更新 SharpLang 了。
其他
一些相关技术的图片:
.NET
实际上无论是 JVM 还是 .NET Framework 以及 LLVM Framework 在结构上是非常相似的,如下图:
.NET 的功能演进:
从源码到运行:
JVM
JVM 加载器:
Web AOT ?
asm.js 是一个非常容易优化的 JavaScript 子集:
asm.js AOT
PNacl
本质上,PNaCl 通过编译本地的 C 和 C++ 代码到一个中间表示,而不是像在 Native Client 的特定于体系结构的表示。LLVM 类型的字节代码被包裹在一个可移植的执行体里面,这个执行体可以托管在一个 Web 服务器上,就像许多其它的网站资产一样。当该网站被访问的时候,Chrome 获取信息并将可移植的执行体转换成一个特定于体系结构的、便携式的、可执行的机器代码,直接为底层设备进行优化。这种转换方法意味着开发者不需要施行多次重新编译App,也可以在x86、ARM或MIPS设备上运行。
WebAssembly
WebAssembly 是 Microsoft Google Mozille Apple 开发者合作开发的一项新技术,可以作为任何编程语言的编译目标, 使应用程序可以运行在浏览器或其它代理中。
InfoQ WebAssembly:面向Web的通用二进制和文本格式
备注
- LLVM http://www.aosabook.org/en/llvm.html
- Embarcadero C++ Builder:
BCC64.EXE, the C++ 64-bit Windows Compiler
Clang-based C++ Compilers - Phoenix Compiler Framework:
Phoenix Compiler Framework Wiki - Android Dalvik:
Dalvik Wiki - LLILC News:
InfoQ Microsoft Introduces LLILC, LLVM-based .NET/CoreCLR Compiler
UPDATE 几年之后,技术有了长足发展,本文并不一定符合当前新的技术形式。
Let me know what you think of this article on twitter @sinopre!