前言
OSR(On-Stack Replacement),按照英文语义意思是在堆栈上替换。堆存托管对象,栈存局部变量以及其它协程(协助程序)机器值。替换这两个东西,基本上就是替换了整个函数运行的机器码。本篇来看下它的运作模式
概括
1.示例
看一个官方的例子:
static void Main()
{
var sw = new System.Diagnostics.Stopwatch();
while (true)
{
sw.Restart();
for (int trial = 0; trial < 10_000; trial++)
{
int count = 0;
for (int i = 0; i < char.MaxValue; i++)
if (IsAsciiDigit((char)i))
count++;
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
}
2.条件
触发堆栈替换的条件有两条
其一,需要开启快速
JIT(DOTNET_TC_QuickJitForLoops)
这点主要是针对.Net6而言,因为它默认没有开启。.Net7及其之后,就会默认开启这一项
其二,当一个函数运行次数超过1000(0x3E8)次的时候,会对它进行堆栈替换。比如示例当中的IsAsciiDigit运行次数超过1000次。
满足以上两个条件之后,IsAsciiDigit函数会被重新高度优化性质的编译。示例当中IsAsciiDigit函数的原来没有超过1000次运行的函数头,会被重新编译之后的函数头的进行一个替换。此后.Net8运行的机器码皆以后者为主。
3.原理
那么它实际上的一个运行原理到底是什么样的呢?非常简单,就是个for循环。参考下面的代码。
static void Main()
{
var sw = new System.Diagnostics.Stopwatch();
for (int xinjian = 0; xinjian <= 0x3E8; xinjian++)
{
if (xinjian >= 0)
{
while (true)
{
sw.Restart();
for (int trial = 0; trial < 10_000; trial++)
{
int count = 0;
for (int i = 0; i < char.MaxValue; i++)
{
if (IsAsciiDigit((char)i))
count++;
--xinjian;
}
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
}
else
{
JIT_Patchpoint();
}
static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
}
}
当进行JIT编译之后,示例代码被替换成了以上代码.它多了一个for循环和判断,调用一次IsAsciiDigit函数,自减一次.最后如果0x23E8此全部减完.那么则调用JIT_Patchpoint函数重新编译,替换掉旧函数头,此后就运行新的函数头.
稍微深入点,可以看看下面的机器码
00007FF81A573B3E E8 4D 0F DF 5E call JIT_TrialAllocSFastMP_InlineGetThread (07FF879364A90h)
00007FF81A573B43 48 89 45 A8 mov qword ptr [rbp-58h],rax
00007FF81A573B47 48 8B 4D A8 mov rcx,qword ptr [rbp-58h]
00007FF81A573B4B FF 15 7F A0 68 00 call qword ptr [7FF81ABFDBD0h]
00007FF81A573B51 48 8B 4D A8 mov rcx,qword ptr [rbp-58h]
00007FF81A573B55 48 89 4D C0 mov qword ptr [rbp-40h],rcx
00007FF81A573B59 C7 45 98 E8 03 00 00 mov dword ptr [rbp-68h],3E8h
00007FF81A573B60 8B 4D 98 mov ecx,dword ptr [rbp-68h]
00007FF81A573B63 FF C9 dec ecx
00007FF81A573B65 89 4D 98 mov dword ptr [rbp-68h],ecx
00007FF81A573B68 83 7D 98 00 cmp dword ptr [rbp-68h],0
00007FF81A573B6C 7F 0E jg 00007FF81A573B7C
00007FF81A573B6E 48 8D 4D 98 lea rcx,[rbp-68h]
00007FF81A573B72 BA 06 00 00 00 mov edx,6
00007FF81A573B77 E8 94 F0 97 5E call JIT_Patchpoint (07FF878EF2C10h)
可以看到OSR是在sw对象实例化之后,进行一个判断的。对0x3E8进行自减,当全部减完,则调用JIT_Patchpoint 。跟上面推测如出一辙。
它的本质code非常复杂,这里只是扼要的结果。
结尾
作者:江湖评谈。
技术交流:QQ群,676817308,也可加入知识星球讨论你没见过的顶级技术。