目前的CoreCLR触发了JIT编译后, 会在当前线程中执行JIT编译.
如果多个线程同时调用了一个未JIT的函数, 其中一个线程会执行编译, 其他线程会等待编译完成.
CoreCLR会对正在JIT编译的函数分配一个线程锁(ListLockEntry)来实现这一点.
JIT会为准备的函数创建一个Compiler实例, Compiler实例储存了BasicBlock列表等编译时需要的信息.
一个正在编译的函数对应一个Compiler实例, 函数编译后Compiler实例会被销毁.
接下来我会对JIT的各项步骤进行一个简单的说明.
Frontend ImporterImporter负责读取和解析IL(byte array), 并根据IL生成JIT使用的内部表现IR(BasicBlock, Statement, GenTree).
BasicBlock会根据它们的跳转类型连接成一个图(graph).
第一个BasicBlock是内部使用的, 会添加一些函数进入的初始化处理(但不要和汇编中的prolog混淆).
下图是Importer的实例:
Inliner如果函数符合内联的条件, 则Inliner会把函数的IR嵌入到它的调用端函数(callsite), 并且对本地变量和参数进行修整.
执行内联后接下来的步骤将在调用端函数中完成.
内联的条件有很多, 判断逻辑也相当的复杂, 这里我只列出一部分:
下图是Inliner的实例:
MorphMorph会对Importer导入的HIR进行变形, 这个步骤包含了很多处理, 这里我只列出一部分:
经过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的实例, 红线表示连接下一个节点:
Optimize SSA & VNRyuJIT为了实现更好的优化, 会对GenTree节点分配SSA序号和VN.
要说明什么是SSA, 可以拿Wikipedia上的代码做例子:
这里有4个BasicBlock和3个变量(x, y, w), 变量的值会随着执行而改变,
我们很难确定两个时点的y是否同一个y, 这为代码优化带来了障碍.