不少C#的初學者都會有這麼一個疑問, .Net程序代碼是如何被機器加載執行的?
最簡單的解答是, C#會經過編譯器(CodeDom, Roslyn)編譯成IL代碼,
而後CLR(.Net Framework, .Net Core, Mono)會把這些IL代碼編譯成目標機器的機器代碼並執行.
相信大多數的C#的書籍都是這樣一筆帶過的.
這篇和下篇文章會深刻講解JIT的具體工做流程,
和前面的GC篇同樣, 實現中的不少細節都是無標準文檔的, 用搜索引擎不會找到它們相關的資料.html
由於內容至關多, 講解JIT的文章將會分爲兩篇.
第一篇是入門篇, 看過這個系列以前的文章和CLR via C#, 瞭解一些編譯原理的均可以看的明白.
第二篇是詳解篇, 會分析JIT的具體實現流程, 算法和數據結構.前端
這篇的內容是基於CoreCLR 1.1.0分析的, 其餘CLR中的實現不必定和這篇分析的實現徹底同樣.
微軟最近提供了一篇JIT入門文檔,
儘管裏面寫的至關潦草可是仍有很大的參考價值, 推薦同時參考這個文檔.git
相信不少C#程序員都知道, 咱們編寫的C#代碼在通過編譯後得出的exe或dll裏面包含的並非機器代碼,
而是一種中間代碼, 也稱爲MSIL(簡稱IL).
MSIL能夠在不一樣的系統或者平臺上執行, CLR中執行它們的模塊就是這篇要講的JIT.程序員
如圖所示github
CoreCLR中的JIT代號是RyuJIT, RyuJIT能夠把MSIL翻譯爲X86, X64或者ARM的機器代碼.web
使用JIT的好處有算法
使用JIT的壞處有後端
爲了解決這些壞處而出現的技術有NGEN, AOT, CoreRT等, 可是使用它們之後同時也就失去了使用JIT的好處.數組
如下的圖片來源於微軟提供的JIT入門文檔:安全
整體上來講RyuJIT能夠分爲兩個部分.
前端: 也就是圖上的第一行, 負責把MSIL轉換爲JIT中的內部表現(IR)而且執行優化.
後端: 也就是圖上的第二行, 負責準備生產機器代碼, 分配寄存器等與平臺相關的處理.
具體的步驟能夠分爲:
前端的步驟有(導入MSIL和優化):
後端的步驟有(平臺相關的處理):
只看上面的圖你可能會一頭霧水, 咱們來看看實際的流程.
爲了更容易理解這裏我使用的是Debug模式.
如下的內容來源於CoreCLR的輸出, 設置環境變量"COMPlus_JitDump=Main"而且使用Debug版的CoreCLR便可獲得.
首先是C#代碼, 很是簡單的循環3次而且輸出到控制檯.
using System; using System.Runtime.InteropServices; namespace ConsoleApplication { public class Program { public static void Main(string[] args) { for (int x = 0; x < 3; ++x) { Console.WriteLine(x); } } } }
通過編譯後會生成如下的IL, 下面我標記了運行堆棧的狀態和簡單的註釋.
IL to import: IL_0000 00 nop IL_0001 16 ldc.i4.0 ; 運行堆棧 [ 0 ] IL_0002 0a stloc.0 ; 運行堆棧 [ ], 保存到本地變量0 (x = 0) IL_0003 2b 0d br.s 13 (IL_0012) ; 跳轉到IL_0012 IL_0005 00 nop IL_0006 06 ldloc.0 ; 運行堆棧 [ x ] IL_0007 28 0c 00 00 0a call 0xA00000C ; 運行堆棧 [ ], 調用Console.WriteLine, 這裏的0xA00000C是token IL_000c 00 nop IL_000d 00 nop IL_000e 06 ldloc.0 ; 運行堆棧 [ x ] IL_000f 17 ldc.i4.1 ; 運行堆棧 [ x, 1 ] IL_0010 58 add ; 運行堆棧 [ x+1 ] IL_0011 0a stloc.0 ; 運行堆棧 [ ], 保存到本地變量0 (x = x + 1) IL_0012 06 ldloc.0 ; 運行堆棧 [ x ] IL_0013 19 ldc.i4.3 ; 運行堆棧 [ x, 3 ] IL_0014 fe 04 clt ; 運行堆棧 [ x<3 ] IL_0016 0b stloc.1 ; 運行堆棧 [ ], 保存到本地變量1 (tmp = x < 3) IL_0017 07 ldloc.1 ; 運行堆棧 [ tmp ] IL_0018 2d eb brtrue.s -21 (IL_0005); 運行堆棧 [ ], 若是tmp爲true則跳轉到IL_0005 IL_001a 2a ret ; 從函數返回
RyuJIT的前端會把IL導入爲中間表現(IR), 以下
Importing BB02 (PC=000) of 'ConsoleApplication.Program:Main(ref)' [ 0] 0 (0x000) nop [000004] ------------ * stmtExpr void (IL 0x000... ???) [000003] ------------ \--* no_op void [ 0] 1 (0x001) ldc.i4.0 0 [ 1] 2 (0x002) stloc.0 [000008] ------------ * stmtExpr void (IL 0x001... ???) [000005] ------------ | /--* const int 0 [000007] -A---------- \--* = int [000006] D------N---- \--* lclVar int V01 loc0 [ 0] 3 (0x003) br.s [000010] ------------ * stmtExpr void (IL 0x003... ???) [000009] ------------ \--* nop void Importing BB03 (PC=005) of 'ConsoleApplication.Program:Main(ref)' [ 0] 5 (0x005) nop [000025] ------------ * stmtExpr void (IL 0x005... ???) [000024] ------------ \--* no_op void [ 0] 6 (0x006) ldloc.0 [ 1] 7 (0x007) call 0A00000C [000029] ------------ * stmtExpr void (IL 0x006... ???) [000027] --C-G------- \--* call void System.Console.WriteLine [000026] ------------ arg0 \--* lclVar int V01 loc0 [ 0] 12 (0x00c) nop [000031] ------------ * stmtExpr void (IL 0x00C... ???) [000030] ------------ \--* no_op void [ 0] 13 (0x00d) nop [000033] ------------ * stmtExpr void (IL 0x00D... ???) [000032] ------------ \--* no_op void [ 0] 14 (0x00e) ldloc.0 [ 1] 15 (0x00f) ldc.i4.1 1 [ 2] 16 (0x010) add [ 1] 17 (0x011) stloc.0 [000039] ------------ * stmtExpr void (IL 0x00E... ???) [000035] ------------ | /--* const int 1 [000036] ------------ | /--* + int [000034] ------------ | | \--* lclVar int V01 loc0 [000038] -A---------- \--* = int [000037] D------N---- \--* lclVar int V01 loc0 Importing BB04 (PC=018) of 'ConsoleApplication.Program:Main(ref)' [ 0] 18 (0x012) ldloc.0 [ 1] 19 (0x013) ldc.i4.3 3 [ 2] 20 (0x014) clt [ 1] 22 (0x016) stloc.1 [000017] ------------ * stmtExpr void (IL 0x012... ???) [000013] ------------ | /--* const int 3 [000014] ------------ | /--* < int [000012] ------------ | | \--* lclVar int V01 loc0 [000016] -A---------- \--* = int [000015] D------N---- \--* lclVar int V02 loc1 [ 0] 23 (0x017) ldloc.1 [ 1] 24 (0x018) brtrue.s [000022] ------------ * stmtExpr void (IL 0x017... ???) [000021] ------------ \--* jmpTrue void [000019] ------------ | /--* const int 0 [000020] ------------ \--* != int [000018] ------------ \--* lclVar int V02 loc1 Importing BB05 (PC=026) of 'ConsoleApplication.Program:Main(ref)' [ 0] 26 (0x01a) ret [000042] ------------ * stmtExpr void (IL 0x01A... ???) [000041] ------------ \--* return void
咱們能夠看到IL被分紅了好幾組(BB02~BB05), 這裏的BB是BasicBlock的縮寫,
一個BasicBlock中有多個語句(Statement), 一個語句就是一棵樹(GenTree).
上面的文本對應瞭如下的結構(又稱HIR結構):
BasicBlock: 保存了一組語句, BasicBlock內原則上跳轉指令只會出如今最後一個語句
Statement: 一個語句就是一棵樹, 在內部Statement也是一個GenTree的子類(GenTreeStmt)
GenTree: 組成樹的節點, 有不少不一樣的類型例如GenTreeUnOp(unary op), GenTreeIntCon(int constant)
有人可能會好奇爲何上面的BasicBlock從02開始, 這是由於01是內部用的block, 裏面會保存函數開始時運行的內部處理.
接下來RyuJIT的前端會不斷的修改HIR結構, 作出各類變形和優化:
Trees before IR Rationalize ------------------------------------------------------------------------------------------------------------------------------------- BBnum descAddr ref try hnd preds weight [IL range] [jump] [EH region] [flags] ------------------------------------------------------------------------------------------------------------------------------------- BB01 [00000000024701F8] 1 1 [???..???) i internal label target BB02 [0000000002473350] 1 BB01 1 [???..???)-> BB04 ( cond ) internal BB03 [0000000002473460] 1 BB02 0.5 [???..???) internal BB04 [0000000002473240] 2 BB02,BB03 1 [???..???) i internal label target BB05 [0000000002470470] 1 BB04 1 [000..005)-> BB07 (always) i BB06 [0000000002470580] 1 BB07 1 [005..012) i label target gcsafe bwd BB07 [0000000002470690] 2 BB05,BB06 1 [012..01A)-> BB06 ( cond ) i label target bwd BB08 [00000000024707A0] 1 BB07 1 [01A..01B) (return) i ------------------------------------------------------------------------------------------------------------------------------------- ------------ BB01 [???..???), preds={} succs={BB02} ***** BB01, stmt 1 ( 0, 0) [000001] ------------ * stmtExpr void (IL ???... ???) N001 ( 0, 0) [000000] ------------ \--* nop void ------------ BB02 [???..???) -> BB04 (cond), preds={BB01} succs={BB03,BB04} ***** BB02, stmt 2 ( 9, 16) [000055] ------------ * stmtExpr void (IL ???... ???) N005 ( 9, 16) [000054] ------------ \--* jmpTrue void N003 ( 1, 1) [000045] ------------ | /--* const int 0 N004 ( 7, 14) [000046] J------N---- \--* == int N002 ( 5, 12) [000044] ------------ \--* indir int N001 ( 3, 10) [000043] ------------ \--* const(h) long 0x7f95ea870610 token ------------ BB03 [???..???), preds={BB02} succs={BB04} ***** BB03, stmt 3 ( 14, 5) [000056] ------------ * stmtExpr void (IL ???... ???) N001 ( 14, 5) [000047] --C-G-?----- \--* call help void HELPER.CORINFO_HELP_DBG_IS_JUST_MY_CODE ------------ BB04 [???..???), preds={BB02,BB03} succs={BB05} ------------ BB05 [000..005) -> BB07 (always), preds={BB04} succs={BB07} ***** BB05, stmt 4 ( 1, 1) [000004] ------------ * stmtExpr void (IL 0x000...0x000) N001 ( 1, 1) [000003] ------------ \--* no_op void ***** BB05, stmt 5 ( 1, 3) [000008] ------------ * stmtExpr void (IL 0x001...0x002) N001 ( 1, 1) [000005] ------------ | /--* const int 0 N003 ( 1, 3) [000007] -A------R--- \--* = int N002 ( 1, 1) [000006] D------N---- \--* lclVar int V01 loc0 ***** BB05, stmt 6 ( 0, 0) [000010] ------------ * stmtExpr void (IL 0x003...0x003) N001 ( 0, 0) [000009] ------------ \--* nop void ------------ BB06 [005..012), preds={BB07} succs={BB07} ***** BB06, stmt 7 ( 1, 1) [000025] ------------ * stmtExpr void (IL 0x005...0x005) N001 ( 1, 1) [000024] ------------ \--* no_op void ***** BB06, stmt 8 ( 15, 7) [000029] ------------ * stmtExpr void (IL 0x006...0x00C) N005 ( 15, 7) [000027] --C-G------- \--* call void System.Console.WriteLine N003 ( 1, 1) [000026] ------------ arg0 in rdi \--* lclVar int V01 loc0 ***** BB06, stmt 9 ( 1, 1) [000031] ------------ * stmtExpr void (IL 0x00C... ???) N001 ( 1, 1) [000030] ------------ \--* no_op void ***** BB06, stmt 10 ( 1, 1) [000033] ------------ * stmtExpr void (IL 0x00D...0x00D) N001 ( 1, 1) [000032] ------------ \--* no_op void ***** BB06, stmt 11 ( 3, 3) [000039] ------------ * stmtExpr void (IL 0x00E...0x011) N002 ( 1, 1) [000035] ------------ | /--* const int 1 N003 ( 3, 3) [000036] ------------ | /--* + int N001 ( 1, 1) [000034] ------------ | | \--* lclVar int V01 loc0 N005 ( 3, 3) [000038] -A------R--- \--* = int N004 ( 1, 1) [000037] D------N---- \--* lclVar int V01 loc0 ------------ BB07 [012..01A) -> BB06 (cond), preds={BB05,BB06} succs={BB08,BB06} ***** BB07, stmt 12 ( 10, 6) [000017] ------------ * stmtExpr void (IL 0x012...0x016) N002 ( 1, 1) [000013] ------------ | /--* const int 3 N003 ( 6, 3) [000014] ------------ | /--* < int N001 ( 1, 1) [000012] ------------ | | \--* lclVar int V01 loc0 N005 ( 10, 6) [000016] -A------R--- \--* = int N004 ( 3, 2) [000015] D------N---- \--* lclVar int V02 loc1 ***** BB07, stmt 13 ( 7, 6) [000022] ------------ * stmtExpr void (IL 0x017...0x018) N004 ( 7, 6) [000021] ------------ \--* jmpTrue void N002 ( 1, 1) [000019] ------------ | /--* const int 0 N003 ( 5, 4) [000020] J------N---- \--* != int N001 ( 3, 2) [000018] ------------ \--* lclVar int V02 loc1 ------------ BB08 [01A..01B) (return), preds={BB07} succs={} ***** BB08, stmt 14 ( 0, 0) [000042] ------------ * stmtExpr void (IL 0x01A...0x01A) N001 ( 0, 0) [000041] ------------ \--* return void
上面的內容目前能夠不用理解, 我貼出來只是爲了說明HIR結構通過了轉換和變形.
接下來就會進入RyuJIT的後端, RyuJIT的後端會根據HIR結構生成LIR結構:
Trees after IR Rationalize ------------------------------------------------------------------------------------------------------------------------------------- BBnum descAddr ref try hnd preds weight [IL range] [jump] [EH region] [flags] ------------------------------------------------------------------------------------------------------------------------------------- BB01 [00000000024701F8] 1 1 [???..???) i internal label target LIR BB02 [0000000002473350] 1 BB01 1 [???..???)-> BB04 ( cond ) internal LIR BB03 [0000000002473460] 1 BB02 0.5 [???..???) internal LIR BB04 [0000000002473240] 2 BB02,BB03 1 [???..???) i internal label target LIR BB05 [0000000002470470] 1 BB04 1 [000..005)-> BB07 (always) i LIR BB06 [0000000002470580] 1 BB07 1 [005..012) i label target gcsafe bwd LIR BB07 [0000000002470690] 2 BB05,BB06 1 [012..01A)-> BB06 ( cond ) i label target bwd LIR BB08 [00000000024707A0] 1 BB07 1 [01A..01B) (return) i LIR ------------------------------------------------------------------------------------------------------------------------------------- ------------ BB01 [???..???), preds={} succs={BB02} N001 ( 0, 0) [000000] ------------ nop void ------------ BB02 [???..???) -> BB04 (cond), preds={BB01} succs={BB03,BB04} N001 ( 3, 10) [000043] ------------ t43 = const(h) long 0x7f95ea870610 token /--* t43 long N002 ( 5, 12) [000044] ------------ t44 = * indir int N003 ( 1, 1) [000045] ------------ t45 = const int 0 /--* t44 int +--* t45 int N004 ( 7, 14) [000046] J------N---- t46 = * == int /--* t46 int N005 ( 9, 16) [000054] ------------ * jmpTrue void ------------ BB03 [???..???), preds={BB02} succs={BB04} N001 ( 14, 5) [000047] --C-G-?----- call help void HELPER.CORINFO_HELP_DBG_IS_JUST_MY_CODE ------------ BB04 [???..???), preds={BB02,BB03} succs={BB05} ------------ BB05 [000..005) -> BB07 (always), preds={BB04} succs={BB07} ( 1, 1) [000004] ------------ il_offset void IL offset: 0 N001 ( 1, 1) [000003] ------------ no_op void ( 1, 3) [000008] ------------ il_offset void IL offset: 1 N001 ( 1, 1) [000005] ------------ t5 = const int 0 /--* t5 int N003 ( 1, 3) [000007] DA---------- * st.lclVar int V01 loc0 ( 0, 0) [000010] ------------ il_offset void IL offset: 3 N001 ( 0, 0) [000009] ------------ nop void ------------ BB06 [005..012), preds={BB07} succs={BB07} ( 1, 1) [000025] ------------ il_offset void IL offset: 5 N001 ( 1, 1) [000024] ------------ no_op void ( 15, 7) [000029] ------------ il_offset void IL offset: 6 N003 ( 1, 1) [000026] ------------ t26 = lclVar int V01 loc0 /--* t26 int arg0 in rdi N005 ( 15, 7) [000027] --C-G------- * call void System.Console.WriteLine ( 1, 1) [000031] ------------ il_offset void IL offset: 12 N001 ( 1, 1) [000030] ------------ no_op void ( 1, 1) [000033] ------------ il_offset void IL offset: 13 N001 ( 1, 1) [000032] ------------ no_op void ( 3, 3) [000039] ------------ il_offset void IL offset: 14 N001 ( 1, 1) [000034] ------------ t34 = lclVar int V01 loc0 N002 ( 1, 1) [000035] ------------ t35 = const int 1 /--* t34 int +--* t35 int N003 ( 3, 3) [000036] ------------ t36 = * + int /--* t36 int N005 ( 3, 3) [000038] DA---------- * st.lclVar int V01 loc0 ------------ BB07 [012..01A) -> BB06 (cond), preds={BB05,BB06} succs={BB08,BB06} ( 10, 6) [000017] ------------ il_offset void IL offset: 18 N001 ( 1, 1) [000012] ------------ t12 = lclVar int V01 loc0 N002 ( 1, 1) [000013] ------------ t13 = const int 3 /--* t12 int +--* t13 int N003 ( 6, 3) [000014] ------------ t14 = * < int /--* t14 int N005 ( 10, 6) [000016] DA---------- * st.lclVar int V02 loc1 ( 7, 6) [000022] ------------ il_offset void IL offset: 23 N001 ( 3, 2) [000018] ------------ t18 = lclVar int V02 loc1 N002 ( 1, 1) [000019] ------------ t19 = const int 0 /--* t18 int +--* t19 int N003 ( 5, 4) [000020] J------N---- t20 = * != int /--* t20 int N004 ( 7, 6) [000021] ------------ * jmpTrue void ------------ BB08 [01A..01B) (return), preds={BB07} succs={} ( 0, 0) [000042] ------------ il_offset void IL offset: 26 N001 ( 0, 0) [000041] ------------ return void
咱們能夠看到在LIR結構裏, BasicBlock包含的是GenTree節點的有序列表, 原來是樹結構的節點如今都連成了一串.
LIR結構跟最終生成的機器代碼結構很是的類似.
接下來RyuJIT的後端會給LIR結構中的GenTree節點分配寄存器, 而且根據LIR結構生成彙編指令列表:
Instructions as they come out of the scheduler G_M21556_IG01: ; func=00, offs=000000H, size=0016H, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, nogc <-- Prolog IG IN001b: 000000 55 push rbp IN001c: 000001 4883EC10 sub rsp, 16 IN001d: 000005 488D6C2410 lea rbp, [rsp+10H] IN001e: 00000A 33C0 xor rax, rax IN001f: 00000C 8945F4 mov dword ptr [rbp-0CH], eax IN0020: 00000F 8945F0 mov dword ptr [rbp-10H], eax IN0021: 000012 48897DF8 mov gword ptr [rbp-08H], rdi G_M21556_IG02: ; func=00, offs=000016H, size=0014H, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, isz IN0001: 000016 48B8100687EA957F0000 mov rax, 0x7F95EA870610 IN0002: 000020 833800 cmp dword ptr [rax], 0 IN0003: 000023 7405 je SHORT G_M21556_IG03 [02479BA8] ptr arg pop 0 IN0004: 000025 E8D6E0B578 call CORINFO_HELP_DBG_IS_JUST_MY_CODE G_M21556_IG03: ; func=00, offs=00002AH, size=0009H, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, isz IN0005: 00002A 90 nop IN0006: 00002B 33FF xor edi, edi IN0007: 00002D 897DF4 mov dword ptr [rbp-0CH], edi IN0008: 000030 90 nop IN0009: 000031 EB13 jmp SHORT G_M21556_IG05 G_M21556_IG04: ; func=00, offs=000033H, size=0013H, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref IN000a: 000033 90 nop IN000b: 000034 8B7DF4 mov edi, dword ptr [rbp-0CH] [02479BC0] ptr arg pop 0 IN000c: 000037 E864F7FFFF call System.Console:WriteLine(int) IN000d: 00003C 90 nop IN000e: 00003D 90 nop IN000f: 00003E 8B45F4 mov eax, dword ptr [rbp-0CH] IN0010: 000041 FFC0 inc eax IN0011: 000043 8945F4 mov dword ptr [rbp-0CH], eax G_M21556_IG05: ; func=00, offs=000046H, size=0019H, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, byref, isz IN0012: 000046 8B7DF4 mov edi, dword ptr [rbp-0CH] IN0013: 000049 83FF03 cmp edi, 3 IN0014: 00004C 400F9CC7 setl dil IN0015: 000050 400FB6FF movzx rdi, dil IN0016: 000054 897DF0 mov dword ptr [rbp-10H], edi IN0017: 000057 8B7DF0 mov edi, dword ptr [rbp-10H] IN0018: 00005A 85FF test edi, edi IN0019: 00005C 75D5 jne SHORT G_M21556_IG04 IN001a: 00005E 90 nop G_M21556_IG06: ; func=00, offs=00005FH, size=0006H, epilog, nogc, emitadd IN0022: 00005F 488D6500 lea rsp, [rbp] IN0023: 000063 5D pop rbp IN0024: 000064 C3 ret
最後Emitter把這些指令編碼成機器代碼就完成了JIT的編譯工做.
如下的圖片來源於微軟提供的JIT入門文檔:
第一張是HIR的數據結構
第二張是LIR的數據結構
第三張是CoreCLR中實際的數據結構(HIR和LIR會共用GenTree節點).
在至關多的.NET書籍中都提到過, CLR中的JIT是懶編譯的, 那麼具體是如何實現的?
JIT針對每一個函數都會提供一個"樁"(Stub), 第一次調用時會觸發JIT編譯, 第二次調用時會跳轉到第一次的編譯結果.
流程參考下圖:
JIT以前的樁(例子)
0x7fff7c21f5a8: e8 2b 6c fe ff callq 0x7fff7c2061d8
JIT以後的樁(例子)
0x7fff7c21f5a8: e9 a3 87 3a 00 jmp 0x7fff7c5c7d50
具體的彙編代碼分析我會在下一篇中給出, 目前你只須要理解"樁"起到的是一個路由的做用.
目前的CoreCLR觸發了JIT編譯後, 會在當前線程中執行JIT編譯.
若是多個線程同時調用了一個未JIT的函數, 其中一個線程會執行編譯, 其餘線程會等待編譯完成.
CoreCLR會對正在JIT編譯的函數分配一個線程鎖(ListLockEntry)來實現這一點.
JIT會爲準備的函數建立一個Compiler實例, Compiler實例儲存了BasicBlock列表等編譯時須要的信息.
一個正在編譯的函數對應一個Compiler實例, 函數編譯後Compiler實例會被銷燬.
接下來我會對JIT的各項步驟進行一個簡單的說明.
Importer負責讀取和解析IL(byte array), 並根據IL生成JIT使用的內部表現IR(BasicBlock, Statement, GenTree).
BasicBlock會根據它們的跳轉類型鏈接成一個圖(graph).
第一個BasicBlock是內部使用的, 會添加一些函數進入的初始化處理(但不要和彙編中的prolog混淆).
下圖是Importer的實例:
若是函數符合內聯的條件, 則Inliner會把函數的IR嵌入到它的調用端函數(callsite), 而且對本地變量和參數進行修整.
執行內聯後接下來的步驟將在調用端函數中完成.
內聯的條件有不少, 判斷邏輯也至關的複雜, 這裏我只列出一部分:
下圖是Inliner的實例:
Morph會對Importer導入的HIR進行變形, 這個步驟包含了不少處理, 這裏我只列出一部分:
a = 5
便可斷言a等於5, b = new X()
便可斷言b != null
通過Morph變形後的HIR將會包含更多信息, 對IL中隱式的處理(例如邊界檢查和溢出檢查)也添加了顯式的代碼(GenTree).
下圖是Morph的實例:
圖中的comma表示的是逗號式, 例如(X(), 123)
這個式會先評價X()
而後結果使用123,
上圖中的comma會先把數組保存到一個臨時變量, 執行邊界檢查, 而後再訪問數組中的元素而後輸出到控制檯.
Flowgraph Analysis會對BasicBlock進行流程分析,
找出BasicBlock有哪些前任block(predecessor)和後繼block(successor), 而且標記BasicBlock的引用次數.
若是一個block是多個block的跳轉目標, 則這個block有多個preds,
若是一個block的跳轉類型是jtrue(條件成立時跳轉到目標block, 不然到下一個block), 則這個block有兩個succs.
而且計算DOM(dominator)樹,
例如出現 A -> B, A -> C, B -> D, C -> D, 則D的dominator不是B或C而是A, 表示執行D必須通過A,
參考Wikipedia和論文.
例如在這張圖中:
計算出來的DOM(dominator)樹爲:
而後會根據流程分析的結果進行一些優化:
優化 while 到 do while:
優化前 jmp test; loop: ...; test: cond; jtrue loop;
優化後 cond; jfalse done; loop: ...; test: cond; jtrue loop; done: ...;
優化循環中數組的邊界檢查:
優化前 for (var x = 0; x < a.Length; ++x) { b[x] = a[x]; },
優化後
if (x < a.Length) { if ((a != null && b != null) && (a.Length <= b.Length)) { do { var tmp = a[x]; // no bounds check b[x] = tmp; // no bounds check x = x + 1; } while (x < a.Length); } else { do { var tmp = a[x]; b[x] = tmp; x = x + 1; } while (x < a.Length); } }
優化次數是常量的循環:
優化前 for (var x = 0; x < 3; ++x) { DoSomething(); }
優化後 DoSomething(); DoSomething(); DoSomething();
注意循環次數過多或者循環中的代碼過長則不會執行這項優化.
這個步驟會標記函數中本地變量的引用計數, 而且按引用計數排序本地變量表.
而後會對tree的運行運行順序執行標記, 例如 a() + b()
, 會標記a()
先於b()
執行.
(與C, C++不一樣, .Net中對操做參數的運行順序有很嚴格的規定, 例如a+b
和f(a, b)
的運行順序都是已規定的)
通過運行順序標記後其實就已經造成了LIR結構.
LIR結構中無語句(Statement)節點, 語句節點通過在後面的Rationalization後會變爲IL_OFFSET節點, 用於對應的IL偏移值,
最終VisualStudio等IDE能夠根據機器代碼地址=>IL偏移值=>C#代碼偏移值
來下斷點和調試.
下圖是Tree Ordering的實例, 紅線表示鏈接下一個節點:
RyuJIT爲了實現更好的優化, 會對GenTree節點分配SSA序號和VN.
要說明什麼是SSA, 能夠拿Wikipedia上的代碼作例子:
這裏有4個BasicBlock和3個變量(x, y, w), 變量的值會隨着執行而改變,
咱們很難肯定兩個時點的y是否同一個y, 這爲代碼優化帶來了障礙.
爲了解決這個問題咱們爲每一個變量都標記一個版本號, 修改一次它的值就會出現一個新的版本.
這就是SSA(Static single assignment form), 一個變量+版本只能有一個值, 這時咱們能夠很簡單的肯定兩個時點的y是否同一個y.
可是上圖有一個問題, 最後一個BasicBlock使用的y在編譯時是沒法肯定來源於哪一個版本的.
爲了解決這個問題, SSA引入了Φ(Phi)函數, 最後一個BasicBlock的開頭添加一個新的版本y3 = Φ(y1, y2)
.
而VN(Value Number)則是基於SSA的標記, 會根據給GenTree分配一個惟一的ID, 例如x = 3
和w = 3
時, x和w的VN會相等.
下圖是標記SSA和VN的實例:
上面的"Flowgraph Analysis"提到的針對循環的一些優化, 在生成了SSA和VN之後咱們能夠作出進一步的優化.
例如這樣的循環:
var a = SomeFunction(); for (var x = 0; x < 3; ++x) { Console.WriteLine(a * 3); }
注意a * 3
這個表達式, 它每次循環都是同樣的而且無反作用, 也就是咱們能夠提取(hoist)它到循環外面:
var a = SomeFunction(); var tmp = a * 3; for (var x = 0; x < 3; ++x) { Console.WriteLine(tmp); }
這樣a * 3
咱們就只須要計算一次了, 但須要注意的是這種優化會增長一個臨時變量, 因此實際不必定會執行.
這項優化會替換具備相同VN的本地變量,
例如var tmp = a; var b = tmp + 1;
, 由於咱們肯定tmp和a的值(VN)是一致的, 能夠優化爲var b = a + 1
.
在執行這項優化後, 多餘的臨時變量將再也不須要, 例如上面的tmp
變量若是引用計數爲0便可刪除.
這項優化會替換具備相同VN的表達式, 比起Copy Propagation
這項優化的效果更強大.
例如:
var a = SomeFunction(); var b = (a + 5) * a; var c = (a + 5) + a;
注意a + 5
這個表達式出現了兩次, 這兩次對應的GenTree的VN都是同樣的,
由於它們無反作用(不會修改到全局狀態), JIT能夠把這段代碼優化爲:
var a = SomeFunction(); var tmp = a + 5; var b = tmp * a; var c = tmp + a;
和上面的Loop Optimizations同樣, 這種優化會增長一個臨時變量, 因此實際不必定會執行.
在上面的Morph中JIT根據語句添加了一些斷言, 在生成VN後JIT能夠傳播這些斷言.
例如:
var x = 1; // x肯定爲1 var y = x + 2;
傳播斷言後:
var x = 1; // x肯定爲1 var y = x + 2; // y肯定爲3
由於斷言已經傳播, 這項優化能夠根據斷言和VN來判斷哪些數組的邊界檢查是多餘的.
例如:
var length = 100; var index = 99; var a = new int[length]; // a的長度肯定爲100 var b = a[index]; // 肯定訪問不會越界, 因此這裏的邊界檢查能夠去掉
這個步驟會正式把HIR轉換爲LIR, 後面的步驟使用的都是LIR形式.
前面的HIR中存在着一些問題, 例如ASG(=)節點:
看出問題了嗎?lclVar
在LIR中若是不訪問後面的節點, 沒法肯定是讀取變量仍是寫入變量.
Rationalizer會修改這些有問題的GenTree, 讓後面的處理更加簡單.
上面的lclVar =
會修改成st.lclVar
, 與lclVar
區別開來.
這個步驟會修改LIR中的GenTree節點, 讓它更接近最終生成的機器代碼形式.
如下是部分會轉換的GenTree節點:
((v07 << 2) + v01) + 16
能夠轉換爲lea(v01 + v07*4 + 16)
if else
和jmp jumpTable[x-offset]
在完成了對GenTree節點的修改後, Lowering會對每一個節點肯定來源(src)和目標(dst)的寄存器數量.
例如lclVar
節點須要一個目標寄存器, lclVar + lclVar
節點須要兩個來源寄存器和一個目標寄存器.
除了設置須要的寄存器數量外, Lowering還會標記哪些節點是contained
,
標記爲contained
的節點表明它是上級節點的一部分, 生成指令時不須要針對contained
節點單獨生成.
典型的contained
節點是常量, 例如b = a + 1
能夠生成add rbx, 1; mov rdi, rbx;
, 這裏的1並不須要一條單獨的指令.
下圖是Lowering的實例:
在Lowering確認了寄存器需求之後, JIT還須要給這些節點實際的分配寄存器.
分配寄存器的算法有Graph coloring和LSRA等, RyuJIT使用的是LSRA, 和論文中的算法很類似.
使用LSRA算法可讓JIT中分配寄存器所需的計算量更少, 可是分配的結果(執行效率)會比Graph coloring差一些.
在LSRA中有如下概念:
下圖是LSRA的實例:
在這張圖中, Interval 0~2 是本地變量, 這裏只有V01被使用, Interval 3~4是虛擬變量, 用於表示函數返回的結果或傳入的參數.
DEF表示Interval被寫入, USE表示Interval被讀取,
Kill無對應的Interval, 只用於表示指定的寄存器的值是否在某個位置後被破壞,
FixedReg也無對應的Interval, 只用於表示對應的位置使用了固定的寄存器.
在確認Interval和RefPosition後, LSRA會開始分配寄存器,
一個寄存器在同一時間只能被一個Interval使用, 圖上的寄存器都未出現Interval重疊的狀況,
若是出現Interval重疊, 寄存器中的值會保存(spill)到堆棧上的變量.
若是一個變量從未被spill, 則該變量能夠不使用堆棧保存, 如圖上的V01能夠一直存在rbx
中, 不須要保存在內存裏,
這能夠帶來很大幅度的性能提高.
LSRA會積極的使用Callee Saved Register(RBX, RBP, R12, R13, R14, R15)暫存變量,
這些寄存器在調用(call)其它函數後原來的值仍然會被保留, 不須要spill.
在以上步驟都完成後, JIT會根據cpu平臺(x86, x64, arm)生成不同的彙編指令.
在CodeGen中有如下概念:
下圖是CodeGen的實例:
如圖所示, CodeGen會按LIR中的節點和LSRA分配的寄存器信息生成彙編指令, 而且會對指令進行分組儲存在不一樣的IG中.
進入函數的prolog和離開函數的epilog指令也會在這裏添加.
CodeGen還會對彙編指令的大小進行估算, 肯定最多須要分配多少內存才能夠編碼這些指令.
在最後, Emiiter會從LoaderHeap中分配內存, 而且根據instrDesc編碼機器代碼.
下圖是Emitter的實例:
除了寫入機器代碼外, Emiiter還會寫入如下的信息:
最終寫入的函數在內存中的結構以下:
機器代碼的前面是函數頭信息(CodeHeader), 函數頭信息指向真正的函數頭信息(RealCodeHeader), 真正的頭信息中包含了上面提到的信息.
咱們能夠實際在Visual Studio中確認這一點:
圖中的0x7ffa46d0d898
就是CodeHeader的內容, 也是指向RealCodeHeader的指針.
後面的55 57 56 ...
是機器代碼, 表示push rbp; push rdi; push rsi; ...
.
打開0x7ffa46d0d898
能夠看到RealCodeHeader
的內容.
這篇是JIT的入門+科普教程, 爲了讓內容更加易懂我省略了大量的實現細節, 也沒有貼出CoreCLR中的代碼. 在下一篇我將結合CoreCLR中的代碼講解JIT的具體工做流程, 內容會比這一篇難不少, 絕大多數C#程序員只要理解這一篇就很足夠了.