不少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#程序员只要理解这一篇就很足够了.