前言
7月9日微软发布了.NET9 PreView6,本篇看下这个版本的CLR(JIT)第一个改进:代码布局改进。
问题
代码的布局改进,是在RyuJIT的HIR(Hight IR)部分,它实际上是对BB(Basick Block)块的顺序改进。
微软早在.NET6就引入了动态PGO的实验,后续的7,8等版本也进行了BB块的顺序改进,但是BB块的顺序如同顽疾始终受限于代码流程图的的控制,这点一直没变。为了改变这个现状,在过去的几个月围绕重构了 RyuJIT 的流程图数据结构,以消除围绕区块排序的各种限制。
非官方解决
一般来说,代码的流水线指令按照顺序执行。比如执行完前一行代码,后一行继续执行。这样会产生两个概念前任(preds)和继任(succs)。但是有的代码,比如if分支这种类型的代码,它的后一行代码不一定是它需要执行的代码,通过if判断跳转,可能跳转到很多行之后去执行。而优化的地点就在这里。
看一个例子(注意这个例子取自官网,但是多了一行代码:Console.ReadLine()所以分析过程会不太一样。
代码语言:javascript复制var random = new Random();
int number = random.Next(100);
if (number < 99)
{
Console.WriteLine("We're in BB02");
}
else
{
Console.WriteLine("We're in BB03");
}
Console.WriteLine("We're in BB04 -- the successor of BB02 and BB03");
Console.ReadLine();
.NET9 RyuJIT HIR会把以上代码生成五个BB块。C#伪代码如下:
代码语言:javascript复制// bb01 JIT植入的调试代码:CORINFO_HELP_DBG_IS_JUST_MY_CODE
// bb02
var random = new Random();
int number = random.Next(100);
// bb03
if (number < 99)
{
Console.WriteLine("We're in BB02");
}
// bb04
else
{
Console.WriteLine("We're in BB03");
}
// bb05
Console.WriteLine("We're in BB04 -- the successor of BB02 and BB03");
Console.ReadLine();
BB01是JIT植入的调试代码,它的preds和succs如下,这里可以忽略。
代码语言:javascript复制------------ BB01 [0000] [???..???) -> BB02(1) (always), preds={} succs={BB02}
BB02即是初始化随机数,可以看到它有两个分支。
代码语言:javascript复制------------ BB02 [0001] [000..019) -> BB04(0.5),BB03(0.5) (cond), preds={BB01} succs={BB03,BB04}
BB03
代码语言:javascript复制------------ BB03 [0002] [019..028) -> BB05(1) (always), preds={BB02} succs={BB05}
BB04
代码语言:javascript复制------------ BB04 [0003] [028..035) -> BB05(1) (always), preds={BB02} succs={BB05}
BB05
代码语言:javascript复制------------ BB05 [0004] [035..047) (return), preds={BB03,BB04} succs={}
没有优化的代码的流水线执行路径如下:
代码语言:javascript复制BB01->BB02->BB03-BB04->BB05
但加入了if分支,则变成了如下,其中变化的BB03/4是if判断里面的内容
代码语言:javascript复制BB01->BB02->BB03->BB05
BB01->BB02->BB04->BB05
优化点就在这里,JIT会对执行的热点(执行次数较多的代码块)进行排序,假如BB03的热点大于BB04,则可以如下流水执行。注意它这里BB04移动到BB05后面去了,是因为4根本没有执行,所以它的热点在某时刻基本为零。
代码语言:javascript复制BB01->BB02->BB03->BB05->-BB04
反之呢?因为是随机数,如果BB04的热度超过了BB03,则可如下动态优化
代码语言:javascript复制BB01->BB02->BB04->BB05->-BB03
BB块
看下热点BB块3/4的实际情况
1.BB03
代码语言:javascript复制------------ BB03 [0002] [019..028) -> BB05(1) (always), preds={BB02} succs={BB05}
***** BB03 [0002]
STMT00018 ( 0x019[E-] ... 0x019 )
[000036] ----- ----- * NO_OP void
***** BB03 [0002]
STMT00019 ( 0x01A[E-] ... 0x024 )
[000038] --CXG ----- * CALL void System.Console:WriteLine(System.String)
[000058] H---- ----- arg0 in rcx --* CNS_INT(h) ref 'We're in BB02'
***** BB03 [0002]
STMT00020 ( 0x024[E-] ... ??? )
[000039] ----- ----- * NO_OP void
***** BB03 [0002]
STMT00021 ( 0x025[E-] ... 0x025 )
[000040] ----- ----- * NO_OP void
***** BB03 [0002]
STMT00022 ( 0x026[E-] ... 0x026 )
[000041] ----- ----- * NOP void
2.BB04
代码语言:javascript复制------------ BB04 [0003] [028..035) -> BB05(1) (always), preds={BB02} succs={BB05}
***** BB04 [0003]
STMT00009 ( 0x028[E-] ... 0x028 )
[000023] ----- ----- * NO_OP void
***** BB04 [0003]
STMT00010 ( 0x029[E-] ... 0x033 )
[000025] --CXG ----- * CALL void System.Console:WriteLine(System.String)
[000059] H---- ----- arg0 in rcx --* CNS_INT(h) ref 'We're in BB03'
***** BB04 [0003]
STMT00011 ( 0x033[E-] ... ??? )
[000026] ----- ----- * NO_OP void
***** BB04 [0003]
STMT00012 ( 0x034[E-] ... 0x034 )
[000027] ----- ----- * NO_OP void
结尾
.NET9 PreView6的这次优化,实际上是对一些优化方方面的硬骨头进行了敲碎,然后重新组织,配合PGO则是性能的另一大提升。当然它的优化远不止于此,其它的诸如,ARM64 代码生成,代码布局,循环优化,减少地址暴露,AVX10v1 支持,硬件内部代码生成,用于浮点和 SIMD 运算的恒定折叠等,后续继续关注下。