龙空技术网

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

opendotnet 136

前言:

此时姐妹们对“详细解读net中的代码动态编译方法”大致比较重视,各位老铁们都想要了解一些“详细解读net中的代码动态编译方法”的相关内容。那么小编同时在网络上收集了一些关于“详细解读net中的代码动态编译方法””的相关资讯,希望大家能喜欢,各位老铁们快快来学习一下吧!

1.前言

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

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项索引得来的.

其二:

MethodDesc(CLR里的函数描述结构体)的m_bFlags2标志位包含

enum_flag2_IsEligibleForTieredCompilation.

其三:判断分层IsEligibleForTieredCompilation函数

判断当前被JIT编译的函数的描述结构MethodDesc的成员m_bFlags2是否包含了

enum_flag2_IsEligibleForTieredCompilation,如果是则进入分层编译。

4.分层编译线程

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

JIT线程伪代码如下:

m_tier1CallCountingCandidateMethodRecentlyRecorded=falseSArray<MethodDesc *> *methodsPendingCounting = m_methodsPendingCountingForTier1;JitCompile{ for(i=0;i<method.count;i++) { if (methodsPendingCounting != ptr) { 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, ptr); }}

ClrSleepEx停止的时间就是

TieredCompilation_CallCountingDelayMs的值。当JIT线程编译的时间,还不足以设置

m_tier1CallCountingCandidateMethodRecentlyRecorded为trued的时候,说明此函数编译的时间超过了TieredCompilation_CallCountingDelayMs的值,需要分层。所以会运行SetCodeEntryPoint进行函数入口替换。

此后就会在入口点,判断函数调用的次数

(TieredCompilation_CallCountThreshold),如果调用次数超过,则进行分层编译。

这里需要注意下分层编译线程的停止时间,它在把所有需要分层编译的函数替换入口之后就会停止。下一轮,有其它函数新建分层编译线程,继续处理需要分层编译的函数。

以上分层编译的主体部分内容。

标签: #详细解读net中的代码动态编译方法