An ahead-of-time (AOT) compiler is a compiler that implements ahead-of-time compilation. This refers to the act of compiling an intermediate language, such as Java bytecode, .NET Common Intermediate Language (CIL), or IBM System/38 or IBM System i "Technology Independent Machine Interface" code, into a system-dependent binary.
AOT(ahead-of-time 編譯方式),就是將 像Java編譯后得到的中間語言文件 變成和系統相關的 native binary。
Most languages with a managed code runtime that can be compiled to an intermediate language take advantage of just-in-time (JIT)[citation needed]. This, briefly, compiles intermediate code into machine codefor a native run while the intermediate code is executing, which may decrease an application's performance. Ahead-of-time compilation eliminates the need for this step by performing the compilation before execution rather than during execution.
JIT(just-in-time),相當於解釋型語言,運行的時候,相當於是解釋一條語句執行一條語句,即將一條中間的托管的語句翻譯成一條機器語句,然后執行這條機器語句。執行效率較低。
相比JIT,AOT在運行之前就將中間托管代碼翻譯成了機器代碼,不存在JIT的“執行期的翻譯”步驟,所以執行效率比JIT高。
AOT compilation is mostly beneficial in cases where the interpreter (which is small) is too slow or JIT is too complex or introduces undesirable latencies.[citation needed] In most situations with fully AOT compiled programs and libraries it is possible to drop considerable fraction of runtime environment, thus saving disk space, memory and starting time. Because of this it can be useful in embedded or mobile devices.
AOT主要適用場合是,當解釋器(中間語言翻譯成機器語言)速度太慢,或JIT過於復雜,或JIT導致嚴重的滯后的情況。大多數情況下,啟用了AOT以后的可執行程序,相比啟用AOT之前,可以摒棄很多運行時環境的碎片(fraction of runtime environment),從而節省磁盤/內存空間,縮短啟動時間。因此,AOT在嵌入式設備和移動設備中是很有用的。
AOT in most cases produces machine optimized code, just like a 'standard' native compiler. The difference is that AOT transforms the bytecode of an existing virtual machine into machine code. AOT compilers can perform complex and advanced code optimizations which in most cases of JITing will be considered much too costly. On the other hand AOT can't usually perform some optimizations possible in JIT, like runtime profile-guided optimizations, pseudo-constant propagation or indirect/virtual function inlining.
大多數情況下,AOT都可以將二進制代碼針對硬件做優化,這時的AOT就很想一個真正的本地編譯器,比如gcc。AOT和真正的本地編譯器的不同之處在於,AOT是將現有的虛擬機的字節碼轉成本機二進制碼,即操作對象和真正的編譯器不同。AOT編譯器可以執行高級的復雜的字節碼的優化,這些優化對JIT來說,大多數都代價太高。另一方面,通常AOT不能執行一些JIT能做到的優化,比如 runtime profile-guided optimizations, pseudo-constant propagation or indirect/virtual function inlining。
一些資料:
IL
IL是.NET框架中中間語言(Intermediate Language)的縮寫。使用.NET框架提供的編譯器可以直接將源程序編譯為.exe或.dll文件,但此時編譯出來的程序代碼並不是CPU能直接執行的機器代碼,而是一種中間語言IL(Intermediate Language)
優點:
Xamarin
Xamarin(現以被微軟收購)是mono項目的一個分支,但這里面最大的區別Xamarin是商業項目.mono做為跨平台的框架已得到越來越多的商業項目的肯定,令外界擔心的版權問題\可靠性\穩定性也得到證實,使用mono最大的好處是可以使用其它平台眾多的項目解決方案,而不必被限制在windows平台下貧乏而又昂貴的各種解決方案.
在Mac OS上,因為iOS的現有限制,面向iOS的C#代碼會通過AOT編譯技術直接編譯為ARM匯編代碼。而在Android上,應用程序會轉換為IL,啟動時再進行JIT編譯。
Mono在Full AOT模式下的限制
調試時遇到一個Mono運行時異常:
1
2
|
ExecutionEngineException: Attempting to JIT compile method
'...'
while
running with --aot-only.
|
最后發現原因是使用了泛型接口,導致Mono需要JIT編譯,但在iOS平台中,Mono是以Full AOT模式運行的,無法使用JIT引擎,於是引發了這個異常。
Mono的AOT和.NET的Ngen一樣,都是通過提前編譯來減少JIT的工作,但默認情況下AOT並不編譯所有IL代碼,而是在優化和JIT之間取得一個平衡。由於iOS平台禁止JIT編譯,於是Mono在iOS上需要Full AOT編譯和運行。即預先對程序集中的所有IL代碼進行AOT編譯生成一個本地代碼映像,然后在運行時直接加載這個映像而不再使用JIT引擎。目前由於技術或實現上的原因在使用Full AOT時有一些限制,具體可以參考MonoTouch的文檔,這里提幾條常見的:
-
不支持泛型虛方法,因為對於泛型代碼,Mono通過靜態分析以確定要實例化的類型並生成代碼,但靜態分析無法確定運行時實際調用的方法(C++也因此不支持虛模版函數)。
-
不支持對泛型類的P/Invoke。
-
目前不能使用反射中的Property.SetInfo給非空類型賦值。
-
值類型作為Dictionary的Key時會有問題,實際上實現了IEquatable<T>的類型都會有此問題,因為Dictionary的默認構造函數會使用EqualityComparer<TKey>.Default作為比較器,而對於實現了IEquatable<T>的類型,EqualityComparer<TKey>.Default要通過反射來實例化一個實現了IEqualityComparer<TKey>的類(可以參考EqualityComparer<T>的實現)。 解決方案是自己實現一個IEqualityComparer<TKey>,然后使用Dictionary<TKey, TValue>(IEqualityComparer<TKey>)構造器創建Dictionary實例。
-
由於不允許動態生成代碼,不允許使用System.Reflection.Emit,不允許動態創建類型。
-
由於不允許使用System.Reflection.Emit,無法使用DLR及基於DLR的任何語言。
-
不要混淆了Reflection.Emit和反射,所有反射的API均可用
JIT和AOT:
JIT:http://en.wikipedia.org/wiki/Just-in-time_compilation
AOT:http://en.wikipedia.org/wiki/AOT_compiler
http://www.mono-project.com/AOT
那么來到U3D為何能跨平台,簡而言之,其實現原理在於使用了叫CIL(Common Intermediate Language通用中間語言,也叫做MSIL微軟中間語言)的一種代碼指令集,CIL可以在任何支持CLI(Common Language Infrastructure,通用語言基礎結構)的環境中運行,就像.NET是微軟對這一標准的實現,Mono則是對CLI的又一實現。由於CIL能運行在所有支持CLI的環境中,例如剛剛提到的.NET運行時以及Mono運行時,也就是說和具體的平台或者CPU無關。這樣就無需根據平台的不同而部署不同的內容了。所以到這里,各位也應該恍然大了。代碼的編譯只需要分為兩部分就好了嘛:
- 從代碼本身到CIL的編譯(其實之后CIL還會被編譯成一種位元碼,生成一個CLI assembly)
- 運行時從CIL(其實是CLI assembly,不過為了直觀理解,不必糾結這種細節)到本地指令的即時編譯(這就引出了為何U3D官方沒有提供熱更新的原因:在iOS平台中Mono無法使用JIT引擎,而是以Full AOT模式運行的,所以此處說的額即時編譯不包括IOS)
What?
上文也說了CIL是指令集,但是不是還是太模糊了呢?所以語文老師教導我們,描述一個東西時肯定要先從外貌寫起。遵循老師的教導,我們不妨先通過工具來看看CIL到底長什么樣。
工具就是ilasm了。下面小匹夫寫一個簡單的.cs看看生成的CIL代碼長什么樣。
C#代碼:
class Class1 { public static void Main(string[] args) { System.Console.WriteLine("hi"); } }
CIL代碼:
.class private auto ansi beforefieldinit Class1 extends [mscorlib]System.Object { .method public hidebysig static void Main(string[] args) cil managed { .entrypoint // 代碼大小 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldstr "hi" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method Class1::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // 代碼大小 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method Class1::.ctor } // end of class Class1
好啦。代碼雖然簡單,但是也能說明足夠多的問題。那么和CIL的第一次親密接觸,能給我們留下什么直觀的印象呢?
- 以“.”一個點號開頭的,例如上面這份代碼中的:.class、.method 。我們稱之為CIL指令(directive),用於描述.NET程序集總體結構的標記。為啥需要它呢?因為你總得告訴編譯器你處理的是啥吧。
- 貌似CIL代碼中還看到了private、public這樣的身影。姑且稱之為CIL特性(attribute)。它的作用也很好理解,通過CIL指令並不能完全說明.NET成員和類,針對CIL指令進行補充說明成員或者類的特性的。市面上常見的還有:extends,implements等等。
- 每一行CIL代碼基本都有的,對,那就是CIL操作碼咯。小匹夫從網上找了一份漢化的操作碼表放在附錄部分,當然英文版的你的vs就有。
直觀的印象有了,但是離我們的短期目標,說清楚(或者說介紹個大概)CIL是What,甚至是終極目標,搞明白Uniyt3D為何能跨平台還有2萬4千9百里的距離。
好啦,話不多說,繼續亂侃。
參照附錄中的操作碼表,對照可以總結出一份更易讀的表格。那就是如下的表啦。
主要操作 | 操作數范圍/條件 | 操作數類型 | 操作數 | |||||||||
縮寫 | 全稱 | 含義 | 縮寫 | 全稱 | 含義 | 縮寫 | 全稱 | 含義 | 縮寫 | 全稱 | 含義 | |
ld | load | 將操作數壓到堆棧當中,相當於: push ax |
arg | argument | 參數 | ? | ? | 操作數中的數值 | .0 | ? | 第零個參數 | |
.1 | ? | 第一個參數 | ||||||||||
.2 | ? | 第二個參數 | ||||||||||
.3 | ? | 第三個參數 | ||||||||||
.s xx | (short) | 參數xx | ||||||||||
a | address | 操作數的地址 | 只有 .s xx,參見ldarg.s | |||||||||
loc | local | 局部變量 | 參見ldarg | |||||||||
fld | field | 字段(類的全局變量) | 參見ldarg | xx | ? | xx字段,eg: ldfld xx |
||||||
c | const | 常量 | .i4 | int 4 bytes | C#里面的int,其他的類型例如short需要通過conv轉換 | .m1 | minus 1 | -1 | ||||
.0 | ? | 0 | ||||||||||
.1 | ? | 1 | ||||||||||
…… | ||||||||||||
.8 | 8 | |||||||||||
.s | (short) | 后面跟一個字節以內的整型數值(有符號的) | ||||||||||
? | ? | 后面跟四個字節的整型數值 | ||||||||||
.i8 | int 8 bytes | C#里面的long | ? | ? | 后面跟八個字節的整型數值 | |||||||
.r4 | real 4 bytes | C#里面的float | ? | ? | 后面跟四個字節的浮點數值 | |||||||
.r8 | real 8 bytes | C#里面的double | ? | ? | 后面跟八個字節的浮點數值 | |||||||
null | null | 空值(也就是0) | ? | ? | ? | ? | ? | ? | ||||
st | store | 計算堆棧的頂部彈出當前值,相當於: pop ax |
參見ld | |||||||||
conv | convert | 數值類型轉換,僅僅用純粹的數值類型間的轉換,例如int/float等 | ? | ? | ? | .i1 | int 1 bytes | C#里面的sbyte | ? | ? | ? | |
.i2 | int 2 bytes | C#里面的short | ||||||||||
.i4 | int 4 bytes | C#里面的int | ||||||||||
.i8 | int 8 bytes | C#里面的long | ||||||||||
.r4 | real 4 bytes | C#里面的float | ||||||||||
.r8 | real 8 bytes | C#里面的double | ||||||||||
.u4 | uint 4 bytes | C#里面的uint | ||||||||||
.u8 | uint 8 bytes | C#里面的ulong | ||||||||||
b/br | branch | 條件和無條件跳轉,相當於: jmp/jxx label_jump |
br | ? | ? | 無條件跳轉 | ? | ? | ? | ? | ? | 后面跟四個字節的偏移量(有符號) |
.s | (short) | 后面跟一個字節的偏移量(有符號) | ||||||||||
false | false | 值為零的時候跳轉 | ? | ? | ? | 參見br | ||||||
true | true | 值不為零的時候跳轉 | ? | ? | ? | |||||||
b | eq | equal to | 相等 | ? | ? | ? | ||||||
ne | not equal to | 不相等 | un | unsigned or unordered | 無氟好的(對於整數)或者無序的(對於浮點) | |||||||
gt | greater than | 大於 | ||||||||||
lt | less than | 小於 | ||||||||||
ge | greater than or equal to | 大於等於 | ||||||||||
le | less than or equal to | 小於等於 | ||||||||||
call | call | 調用 | ? | ? | ? | ? | ? | (非虛函數) | ? | |||
? | ? | ? | virt | virtual | 虛函數 |
在此,小匹夫想請各位認真讀表,然后心中默數3個數,最后看看都能發現些什么。
基於堆棧
如果是小匹夫的話,第一感覺就是基本每一條描述中都包含一個”棧“。不錯,CIL是基於堆棧的,也就是說CIL的VM(mono運行時)是一個棧式機。這就意味着數據是推入堆棧,通過堆棧來操作的,而非通過CPU的寄存器來操作,這更加驗證了其和具體的CPU架構沒有關系。為了說明這一點,小匹夫舉個例子好啦。
大學時候學單片機(大概是8086,記不清了)的時候記得做加法大概是這樣的:
add eax,-2
其中的eax是啥?寄存器。所以如果CIL處理數據要通過cpu的寄存器的話,那也就不可能和cpu的架構無關了。
當然,CIL之所以是基於堆棧而非CPU的另一個原因是相比較於cpu的寄存器,操作堆棧實在太簡單了。回到剛才小匹夫說的大學時候曾經學過的單片機那門課程上,當時記得各種寄存器,各種標志位,各種。。。,而堆棧只需要簡單的壓棧和彈出,因此對於虛擬機的實現來說是再合適不過了。所以想要更具體的了解CIL基於堆棧這一點,各位可以去看一下堆棧方面的內容。這里小匹夫就不拓展了。
面向對象
那么第二感覺呢?貌似附錄的表中有new對象的語句呀。嗯,的確,CIL同樣是面向對象的。
這意味着什么呢?那就是在CIL中你可以創建對象,調用對象的方法,訪問對象的成員。而這里需要注意的就是對方法的調用。
回到上表中的右上角。對,就是對參數的操作部分。靜態方法和實例方法是不同的哦~
- 靜態方法:ldarg.0么有被占用,所以參數從ldarg.0開始。
- 實例方法:ldarg.0是被this占用的,也就是說實際上的參數是從ldarg.1開始的。
舉個例子:假設你有一個類Murong中有一個靜態方法Add(int32 a, int32 b),實現的內容就如同它的名字一樣使兩個數相加,所以需要2個參數。和一個實例方法TellName(string name),這個方法會告訴你傳入的名字。
class Murong { public void TellName(string name) { System.Console.WriteLine(name); } public static int Add(int a, int b) { return a + b; } }
靜態方法的處理:
那么其中的靜態方法Add的CIL代碼如下:
//小匹夫注釋一下。 .method public hidebysig static int32 Add(int32 a, int32 b) cil managed { // 代碼大小 9 (0x9) .maxstack 2 .locals init ([0] int32 CS$1$0000) //初始化局部變量列表。因為我們只返回了一個int型。所以這里聲明了一個int32類型。索引為0 IL_0000: nop IL_0001: ldarg.0 //將索引為 0 的參數加載到計算堆棧上。 IL_0002: ldarg.1 //將索引為 1 的參數加載到計算堆棧上。 IL_0003: add //計算 IL_0004: stloc.0 //從計算堆棧的頂部彈出當前值並將其存儲到索引 0 處的局部變量列表中。 IL_0005: br.s IL_0007 IL_0007: ldloc.0 //將索引 0 處的局部變量加載到計算堆棧上。 IL_0008: ret //返回該值 } // end of method Murong::Add
那么我們調用這個靜態函數應該就是這樣咯。
Murong.Add(1, 2);
對應的CIL代碼為:
IL_0001: ldc.i4.1 //將整數1壓入棧中 IL_0002: ldc.i4.2 //將整數2壓入棧中 IL_0003: call int32 Murong::Add(int32, int32) //調用靜態方法
可見CIL直接call了Murong的Add方法,而不需要一個Murong的實例。
實例方法的處理:
Murong類中的實例方法TellName()的CIL代碼如下:
.method public hidebysig instance void TellName(string name) cil managed { // 代碼大小 9 (0x9) .maxstack 8 IL_0000: nop IL_0001: ldarg.1 //看到和靜態方法的區別了嗎? IL_0002: call void [mscorlib]System.Console::WriteLine(string) IL_0007: nop IL_0008: ret } // end of method Murong::TellName
看到和靜態方法的區別了嗎?對,第一個參數對應的是ldarg.1中的參數1,而不是靜態方法中的0。因為此時參數0相當於this,this是不用參與參數傳遞的。
那么我們再看看調用實例方法的C#代碼和對應的CIL代碼是如何的。
//C#
Murong murong = new Murong(); murong.TellName("chenjiadong");
CIL:
.locals init ([0] class Murong murong) //因為C#代碼中定義了一個Murong類型的變量,所以局部變量列表的索引0為該類型的引用。 //.... IL_0009: newobj instance void Murong::.ctor() //相比上面的靜態方法的調用,此處new一個新對象,出現了instance方法。 IL_000e: stloc.0 IL_000f: ldloc.0 IL_0010: ldstr "chenjiadong" //小匹夫的名字入棧 IL_0015: callvirt instance void Murong::TellName(string) //實例方法的調用也有instance
到此,受制於篇幅所限(小匹夫不想寫那么多字啊啊啊!)CIL是What的問題大致介紹一下。當然沒有再拓展,以后小匹夫可能會再詳細寫一下這塊。
How?
記得語文老師說過,寫作文最重要的一點是要首尾呼應。既然咱們開篇就提出了U3D為何能跨平台的問題,那么接近文章的結尾咱們就再來
提問:
Q:上面的Why部分,咱們知道了U3D能跨平台是因為存在着一個能通吃的中間語言CIL,這也是所謂跨平台的前提,但是為啥CIL能通吃各大平台呢?當然可以說CIL基於堆棧,跟你CPU怎么架構的沒啥關系,但是感覺過於理論化、學術化,那還有沒有通俗化、工程化的說法呢?
A:原因就是前面小匹夫提到過的,.Net運行時和Mono運行時。也就是說CIL語言其實是運行在虛擬機中的,具體到咱們的U3D也就是mono的運行時了,換言之mono運行的其實CIL語言,CIL也並非真正的在本地運行,而是在mono運行時中運行的,運行在本地的是被編譯后生成的原生代碼。當然看官博的文章,他們似乎也在開發自己的“mono”,也就是被稱為腳本的未來的IL2Cpp,這種類似運行時的功能是將IL再編譯成c++,再由c++編譯成原生代碼,據說效率提升很可觀,小匹夫也是蠻期待的。
這里為了“實現跨平台式的演示”,小匹夫用mac給各位做個測試好啦:
從C#到CIL
新建一個cs文件,然后使用mono來運行。這個cs文件內容如下:
然后咱們直接在命令行中運行這個cs文件試試~
說的很清楚,文件沒有包含一個CIL映像。可見mono是不能直接運行cs文件的。假如我們把它編譯成CIL呢?那么我們用mono帶的mcs來編譯小匹夫的Test.cs文件。
mcs Test.cs
生成了什么呢?如圖:
好像沒見有叫.IL的文件生成啊?反而好像多了一個.exe文件?可是沒聽說Mac能運行exe文件呀?可為啥又生成了.exe呢?各位看官可能要說,小匹夫你是不是拿windows截圖P的啊?嘿嘿,小匹夫可不敢。辣么真相其實就是這個exe並不是讓Mac來運行的,而是留給mono運行時來運行的,換言之這個文件的可執行代碼形式是CIL的位元碼形態。到此,我們完成了從C#到CIL的過程。接下來就讓我們運行下剛剛的成果好啦。
1
|
mono Test.exe
|
結果是輸出了一個大大的“Hi”。這里,就引出了下一個部分。
從CIL到Native Code
這個“HI”可是在小匹夫的MAC終端上出現的呀,那么就證明這個C#寫的代碼在MAC上運行的還挺“嗨”。
為啥呢?為啥C#寫的代碼能跑在MAC上呢?這就不得不提從CIL如何到本機原生代碼的過程了。Mono提供了兩種編譯方式,就是我們經常能看到的:JIT(Just-in-Time compilation,即時編譯)和AOT(Ahead-of-Time,提前編譯或靜態編譯)。這兩種方式都是將CIL進一步編譯成平台的原生代碼。這也是實現跨平台的最后一步。下面就分頭介紹一下。
JIT即時編譯:
從名字就能看的出來,即時編譯,或者稱之為動態編譯,是在程序執行時才編譯代碼,解釋一條語句執行一條語句,即將一條中間的托管的語句翻譯成一條機器語句,然后執行這條機器語句。但同時也會將編譯過的代碼進行緩存,而不是每一次都進行編譯。所以可以說它是靜態編譯和解釋器的結合體。不過你想想機器既要處理代碼的邏輯,同時還要進行編譯的工作,所以其運行時的效率肯定是受到影響的。因此,Mono會有一部分代碼通過AOT靜態編譯,以降低在程序運行時JIT動態編譯在效率上的問題。
不過一向嚴苛的IOS平台是不允許這種動態的編譯方式的,這也是U3D官方無法給出熱更新方案的一個原因。而Android平台恰恰相反,Dalvik虛擬機使用的就是JIT方案。
AOT靜態編譯:
其實Mono的AOT靜態編譯和JIT並非對立的。AOT同樣使用了JIT來進行編譯,只不過是被AOT編譯的代碼在程序運行之前就已經編譯好了。當然還有一部分代碼會通過JIT來進行動態編譯。下面小匹夫就手動操作一下mono,讓它進行一次AOT編譯。
//在命令行輸入 mono --aot Test.exe
結果:
從圖中可以看到JIT time: 39 ms,也就是說Mono的AOT模式其實會使用到JIT,同時我們看到了生成了一個適應小匹夫的MAC的動態庫Test.exe.dylib,而在Linux生成就是.so(共享庫)。
AOT編譯出來的庫,除了包括我們的代碼之外,還有被緩存的元數據信息。所以我們甚至可以只編譯元數據信息而不變異代碼。例如這樣:
//只包含元數據的信息 mono --aot=metadata-only Test.exe
可見代碼沒有被包括進來。
那么簡單總結一下AOT的過程:
- 收集要被編譯的方法
- 使用JIT進行編譯
- 發射(Emitting)經JIT編譯過的代碼和其他信息
- 直接生成文件或者調用本地匯編器或連接器進行處理之后生成文件。(例如上圖中使用了小匹夫本地的gcc)
Full AOT
當然上文也說了,IOS平台是禁止使用JIT的,可看樣子Mono的AOT模式仍然會保留一部分代碼會在程序運行時動態編譯。所以為了破解這個問題,Mono提供了一個被稱為Full AOT的模式。即預先對程序集中的所有CIL代碼進行AOT編譯生成一個本地代碼映像,然后在運行時直接加載這個映像而不再使用JIT引擎。目前由於技術或實現上的原因在使用Full AOT時有一些限制,不過這里不再多說了。以后也還會更細的分析下AOT。
總結:
好啦,寫到現在也已經到了凌晨3:04分了。感覺寫的內容也差不多了。那么對本文的主題U3D為何能跨平台以及CIL做個最終的總結陳詞:
- CIL是CLI標准定義的一種可讀性較低的語言。
- 以.NET或mono等實現CLI標准的運行環境為目標的語言要先編譯成CIL,之后CIL會被編譯,並且以位元碼的形式存在(源代碼--->中間語言的過程)。
- 這種位元碼運行在虛擬機中(.net mono的運行時)。
- 這種位元碼可以被進一步編譯成不同平台的原生代碼(中間語言--->原生代碼的過程)。
- 面向對象
- 基於堆棧
參考
http://m635674608.iteye.com/blog/2034796
https://en.wikipedia.org/wiki/Ahead-of-time_compilation
http://blog.csdn.net/skylin19840101/article/details/44672193
http://www.cnblogs.com/wonderKK/p/4095632.html
http://www.cnblogs.com/me-sa/archive/2012/10/09/erlang_hipe.html