楔子
由于甲方的需求,隨著研究深入,發現CLR編譯函數與ILC編譯是兩種不同的截然方式,除了JIT部分編譯一樣,其它部分貌似完全不一。
本篇來梳理這些東西
示例:
作為例子,先上一段非常簡單的代碼:
internal class Program
{
static void Main(string[] args)
{
A();
}
static void A()
{
B();
}
static void B()
{
}
}
CLR編譯
上面的例子,如果是CLR來編譯,假設從Main函數入口開始,它首先是是通過CLR加載Main函數的IL代碼來調用JIT,構建一個匯編層面代碼。然后跳轉到Main函數的起始位置鏈接放在圖片上層代碼,也就是函數頭處執行。當執行的過程中,遇到了Main函數里面調用了A函數。它會識別A函數的IL代碼,調用JIT把IL代碼編譯成機器碼,然后跳轉到A函數的函數頭,在A函數執行的過程中又遇到B函數,它識別B函數的IL代碼,然后調用JIT把IL代碼編譯成機器碼。跳轉到B函數的函數頭匯編位置執行。執行完畢,然后又跳轉到剛剛調用B函數的A函數的下一條指令開始執行,執行完畢。跳轉到調用A函數的Main函數里面的下一條指令開始執行。Main執行完成之后,整個運行過程執行完畢。
這上面一大段的話語,是自己理解而成。畫成圖片就如下所示:
ILC編譯
1.表位:
ILC的編譯迥異于CLR的編譯,它主要是通過重定位向量表N來構建一個編譯過程。
上面的CLR因為是通過遞歸來查找當前需要編譯的函數鏈接放在圖片上層代碼,這個過程看似沒問題,但是實際上當函數第一次運行的時候,就需要調用JIT。比如某一個函數運行了很多次,但是某次調用了某個特殊函數,這個特殊函數剛好第一次運行(其它時候都沒調用,可能if else這種語句),恰巧如果這個函數編譯時間較長,這樣就拖累整體的運行性能。如果這種情況運行幾次,或者十幾次,或者更多,那么導致整個程序性能拉胯不堪。
2.解決
為了解決這個問題,微軟團隊搞了一個天才的設想。就是把所有的函數,事先編譯一遍,等到運行的時候,直接調用這個編譯之后的結果就行了。剛好社區有AOT編譯需求,于是這個功能就用在了AOT上。
但是AOT的短處也是顯而易見的,最常見的缺陷就是它的優化性能不如JIT。為了解決這個問題,于是R2R技術又天才般的冒出來了。它既解決了AOT優化問題,又解決了JIT預熱問題。
所以就有了ILC與R2R的共享代碼的現狀。
3.過程
這里只是看看,ILC編譯過程,其余不論。ILC是把所有函數全部事先編譯一遍,然后寫入到.O OR .Obj里面,最后鏈接到二進制可執行文件。了解了這個原理,再來看它編譯過程。
如何知道一個函數它所依賴的所有函數呢?比如例子里面,你調用Main函數,Main函數里面又調用了A函數,A函數里面又調用了B函數。
如何通過Main函數知道A函數和B函數的存在,然后把Main,A,B三個函數進行事先編譯呢?
要解決這個問題,需要JIT里面的重定位向量表。ILC在編譯到時候,把所有需要用到的引用進行JIT編譯。
當JIT編譯一個函數的時候,它會在這個函數的匯編代碼層面標記一些東西。比如它編譯Main函數的時候,在Main函數里發現里面調用了A函數,假設上面例子編譯的托管DLL是.Dll。那么A函數就會被注釋成:..A這種形式,然后把它放到基址重定位向量表里。
此后,它會循環被編譯的函數和基址重定位向量表。把編譯的函數添加到全局棧,如果發現函數包含基址向量表,就會把這個向量表進行子循環,把每個向量表里的函數添加到全局站。然后查找向量表里面的函數是否包含函數,如果有,則繼續注釋,放到向量表,循環。
4.圖示
結尾
以上都是通過Debug代碼理解而來,比較晦澀。
純粹是甲方的某些需求需要用到。