.NET8 JIT核心:分层编译的原理

253 阅读3分钟

1.前言

.NET8正式版于今天发布了,很多人期待已久。我们继续来看下核心部分的技术,在JIT里面个人认为核心的部分不是,MSIL二进制BasickBlock,IR中间表示,机器码生成,而是分层编译。本篇来看下它的原理。

2.概述
分层编译在.NET Core2.0中引入,在.NET Core3.0中启用。在.NET8里面已经完全成熟,可以经过分层编译优化(GDV,PGO,OSR,Quick等等)之后的效果达到或者接近C++的性能水准。

在.NET Framework以及.NETCore2.0之前,托管函数被JIT编译之后,它的函数入口点对应的是固定的,无法更改。也就意味着,一旦托管函数被编译,它不能够进行机器码层面的优化。引入分层编译之后,函数的入口点会被编译的函数版本管理类(CodeVersionManager)所管理。当函数运行了一定的次数(有环境变量TieredCompilation_CallCountThreshold控制次数,默认是2次),以及函数编译的时间超过了一定毫秒数(有环境变量tieredCompilation_CallCountingDelayMs控制,默认是1ms)。就会进行分层编译。

它具体的一个运行方式是:当一个模块(你可以理解为托管DLL)的m_dwTransientFlags(这个标志是判断是否把当前模块里的函数进行分层编译)符合分层编译的条件。当JIT编译这个模块的第一个托管函数的时候,会创建一个分层编译的线程。分层编译线程与JIT线程同时运行,分层编译线程会检测当前模块每一个托管函数被JIT编译的时间,如果这个超过tieredCompilation_CallCountingDelayMs环境变量的值,则会把这个函数加入到分层编译函数队列进行函数入口替换。下次每次函数运行的时候,会在TieredCompilation_CallCountThreshold环境变量的值的基础上减1,当TieredCompilation_CallCountThreshold等于0的时候,就会重新进行JIT编译,也就是分层编译优化了。

3.分层判断
分层判断主要是三个方面
其一:m_dwTransientFlags值的判断
上面说了m_dwTransientFlags是托管模块的成员变量,m_dwTransientFlags哪里来的呢?主要是通过托管DLL里面的元数据Blob项索引得来的,操作函数是ComputeDebuggingConfig。
其二:MethodDesc的m_bFlags2标志位包含enum_flag2_IsEligibleForTieredCompilation,操作函数主要是DetermineAndSetIsEligibleForTieredCompilation
其三:判断分层IsEligibleForTieredCompilation
判断当前被JIT编译的函数的描述结构MethodDesc的成员m_bFlags2是否包含了enum_flag2_IsEligibleForTieredCompilation,如果是则进入分层编译。

4.分层编译线程
当JIT判断当前的函数符合分层编译的条件,它就会开启一个新的线程,判断是否有需要进行分层编译的函数。注意JIT线程和分层编译线程的不同,它们是同时并存运行的。如下图所示
image
JIT线程伪代码如下:

m_tier1CallCountingCandidateMethodRecentlyRecorded=false;
SArray<MethodDesc *> *methodsPendingCounting = m_methodsPendingCountingForTier1;
JitCompile
{
   for(i=0;i<method.count;i++)
   {
    if (methodsPendingCounting != nullptr)
    {
     methodsPendingCounting->Append(pMethodDesc);
      if (!m_tier1CallCountingCandidateMethodRecentlyRecorded)
      {
         m_tier1CallCountingCandidateMethodRecentlyRecorded = true;
      }
    }
    NewHolder<SArray<MethodDesc *>>  methodsPendingCountingHolder = new SArray<MethodDesc *>();
    methodsPendingCountingHolder->Preallocate(64);
    methodsPendingCountingHolder->Append(pMethodDesc);
   }
}

注意看m_tier1CallCountingCandidateMethodRecentlyRecorded这个变量,它第一次为false,设置了它自己为true。
分层编译线程伪代码如下:

 while (true)
 {
     _ASSERTE(s_isBackgroundWorkerRunning);
     _ASSERTE(s_isBackgroundWorkerProcessingWork);

     if (IsTieringDelayActive())
     {
         do
         {
             ClrSleepEx(delayMs, false);
         } while (!TryDeactivateTieringDelay());
     }
}
TryDeactivateTieringDelay
{
    if (m_tier1CallCountingCandidateMethodRecentlyRecorded)
   {
      m_tier1CallCountingCandidateMethodRecentlyRecorded = false;
      return false;
   }
    for (COUNT_T i = 0; i < methodCount; ++i)
   {
                   bool wasSet = CallCountingManager::SetCodeEntryPoint(activeCodeVersion, codeEntryPoint, false, nullptr);

   }
}

ClrSleepEx停止的时间就是TieredCompilation_CallCountThreshold的值。当JIT线程编译的时间,还不足以设置m_tier1CallCountingCandidateMethodRecentlyRecorded为trued的时候,说明此函数编译的时间超过了TieredCompilation_CallCountThreshold的值,需要分层。所以会运行SetCodeEntryPoint进行函数入口替换。
此后就会在入口点,判断函数调用的次数,如果调用次数超过,则进行分层编译。

作者:江湖评谈。公众号:jianghupt,欢迎关注,文章首发地。