点击上方蓝字 江湖评谈设为关注
前言
.NET8/9的性能之所以优秀,分层编译的功能功不可没。这需要MSIL的一些特性支撑,进入CLR的第一步就是加载MSIL,因为其独特的加载模式,比较晦涩,本篇看下。
分层特性
分层编译信息观察,需要解决一些特有问题。即如果在Debug模式下,是完全无法获取到分层信息,所以需要设置程序集特性来开启分层信息(参考:.NET普通方法开启分层编译),即设置以下特性
[assembly: Debuggable( DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
这个特性最终被Roslyn Compile到托管DLL里面去了,CLR在加载这个托管DLL的时候,会分析是否设置了此特性,如果有则在Debug的时候开启分层编译信息。是不是这样就可以查看分层信息了呢?并不是,这有一个问题,下面继续看。
问题
以例子说明
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
namespace ConsoleApp1{
internal class Program{
static void ABC({
}
static void Main(string[] args){
ABC();
ABC();
ABC();
ABC();
}
}
}
这个例子设置了DebuggableAttribute程序集特性,并且ABC函数调用了四次。是否开启了分层编译呢?并没有,问题出在哪儿?分层编译的两个条件,其一:函数运行超过两次,其二:函数运行默认时间超过1ms。同时,这里需要了解下分层编译队列的概念,分层编译队列意思是指满足分层编译条件的函数进入到分层编译队列,等待被编译。
这里尤其需要注意,等待被编译,而不是已经或者正在被编译。因为设置了DebuggableAttribute,以及运行超过了两次(本例是4次)ABC函数会进入到分层编译队列,但是编译时间不超过1ms(ABC函数什么都没做,速度非常快)。所以ABC函数只会进入到分层编译队列,但是不会被编译,所以也看不到分层信息。
这种情况下,就需要一个运行时间超过1ms并且满足分层编译条件的函数引导ABC函数进入到分层编译。
以上改造如下:
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
namespace ConsoleApp1{
internal class Program{
static void ABC(){
}
static void Main(string[] args){
ABC();
Console.ReadLine();
ABC();
ABC();
ABC();
}
}
}
改造不同点在于多了一个Console.ReadLine()调用。这个函数是一个系统级的函数在System.Private.CoreLib.dll里面,它是有R2R编译的,全程遵循分层编译的条件。有它引导ABC函数进入了分层编译。
这里的引导进入分层编译的原理是:ABC函数满足进入分层编译队列的条件,但是不满足运行分层编译,而 Console.ReadLine()满足分层编译。分层编译的时候,CLR会把分层编译队列里面的所有函数循环重新编译,此时ABC也会被分层编译了,这里是关键点。
没有Console.ReadLine()之前的ABC函数:
//调用ABC函数
00007FFDB5D2C30E FF 15 F4 15 1E 00 call qword ptr [7FFDB5F0D908h]
//ABC函数的实际部分
00007FFDB5D2C350 55 push rbp
00007FFDB5D2C351 48 8B EC mov rbp,rsp
00007FFDB5D2C354 5D pop rbp
00007FFDB5D2C355 C3 ret
之后:
调用ABC函数
00007FFDB5D1C31A FF 15 E8 15 1E 00 call qword ptr [7FFDB5EFD908h]
ABC函数分层编译判断部分
00007FFDB6580018 48 8B 05 F9 3F 00 00 mov rax,qword ptr [7FFDB6584018h]
00007FFDB658001F 66 FF 08 dec word ptr [rax]
00007FFDB6580022 74 06 je 00007FFDB658002A
00007FFDB6580024 FF 25 F6 3F 00 00 jmp qword ptr [7FFDB6584020h]
00007FFDB658002A FF 25 F8 3F 00 00 jmp qword ptr [7FFDB6584028h]
ABC函数实际部分
00007FFDB5D1C350 55 push rbp
00007FFDB5D1C351 48 8B EC mov rbp,rsp
00007FFDB5D1C354 5D pop rbp
00007FFDB5D1C355 C3 ret
可以看到上面多了一个分层编译判断,如果函数调用小于两次则普通运行,如果大于两次,则进入到分层编译队列。
原理
以上说了分层编译跟MSIL的关系,实际上的运行在CLR。CLR里面DebuggableAttribute特性被识别为DEBUGGABLE_ATTRIBUTE_TYPE,它的意思如下:
#define DEBUGGABLE_ATTRIBUTE_TYPE "System.Diagnostics.DebuggableAttribute"
很明显了,这是一个微软的类库:System.Diagnostics.DebuggableAttribute,Roslyn在Compile带有DebuggableAttribute特性的托管DLL的时候,会在MSIL的Blob项的某个索引处写入如下十六进制:
01 00 07
此十六进制可以通过二进制编辑器观察,比如以下0xB8+0x1==0xB9的位置即是:01 00 07,如下图:
此索引会被存储到MSIL的GetCustomAttribute表的某个索引处的value字段下。GetCustomAttribute表如下:
比如上面GetCustomAttribute表的某个索引处的value字段的值为0x00B8,这里的0x00B8即是Blob项的索引,保存了DebuggableAttribute特性的设置。
CLR识别
CLR加载的时候,会通过GetCustomAttribute表的value值找到DebuggableAttribute的设置,然后判断它的设置是否符合以下值:
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints
本篇以上提到所有的条件都符合之后,则进行进入到分层编译队列。如果不符合,则不进行分层编译。
往期精彩回顾