VC中文网-VC-MFC编程论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1687|回复: 1

全面扫盲:独家解读.NET Core中“分层JIT编译”的内部结构

[复制链接]

16

主题

47

帖子

33

金币

连长

Rank: 7Rank: 7Rank: 7

积分
141

新兵

发表于 2017-12-21 18:38:18 | 显示全部楼层 |阅读模式
.NET运行时(CLR)主要使用JIT编译器将可执行文件转换为机器代码(暂时搁置AOT编译的场景),正如微软公司的官方文档所描述:在执行时,JIT编译器将MSIL (微软中间语言)转换为本地代码。在编译期间,代码必须通过验证过程,检查MSIL和元数据,以确定代码是否可以被确定为类型平安代码。可是这个过程是如何运作的呢?

在JIT编译时,要考虑到在执行过程中可能永远不会挪用某些代码的可能性。而不是使用时间和内存来转换所有的MSIL PE文件中的本地代码,而是在执行过程中根据需要转换MSIL,并将生成的本地代码存储在内存中,以便在该进程的场景中进行后续挪用。加载程序在类型被加载和初始化时建立,并附加一个存根到类型中的每个体例。当第一次挪用某个体例时,存根将控制传递给JIT编译器,JIT编译器将该体例的MSIL转换为本地代码,并修改存根直接指向生成的本机代码。因此,对JIT编译体例的后续挪用将直接转到本机代码。

这真的很简单。可是,如果想知道更多的内容,这篇文章的其余部分将详细探讨这个过程。

另外,人们将看到一个新特性,它正在进入核心CLR(公共语言运行库),称之为“分层编译”。这对CLR来说是一个很年夜的改变,直到现在,.NET体例在第一次使用时才被编译一次。分层编译正在改变这种情况,允许将体例重新编译为更加优化的版本,就像Java Hotspot编译器一样。

是如何工作的

但在考虑未来的计划之前,当前的CLR如何让JIT编译器将一种体例从IL(中间语言)转换为本地代码?那么,其实可以用视图来暗示,因为“一图胜千言”。

该体例被JIT编译之前:
4e80000265e20c210e0b

该体例被JIT编译之后:
509700008c3d5b4e8a2f

需要注意的事情是:

CLR放入了一个“预编码”和“存根”,以便将初始体例挪用转移到PreStubWorker()体例(最终挪用JIT)。这些是程序人员编写的汇编代码片段,只包含几条指令。

一旦这个体例被JIT编译成“本地代码”,就会建立一个稳定的入口点。在CLR的剩余生命周期中包管不会改变,所以余下的运行时间可以连结稳定。

“临时入口点”不会消失,但仍然可用,因为可能有其他体例可以挪用它。然而,相关的“预编码修正”已被重写或“修补”,以指向新建立的“本地代码”而不是PreStubWorker()。

CLR不改变JIT编译的挪用指令的地址,它只改变‘precode’中的地址。可是因为CLR中的所有体例挪用都是通过预编码进行的,所以第二次挪用新的JIT编译后的体例时,挪用将以“本地代码”结束。

作为参考,“稳定的入口点”与挪用RuntimeMethodHandle.GetFunctionPointer()体例时返回的IntPtr的内存位置相同。

如果人们想亲自看到这个过程,可以重新编译CoreCLR源代码,添加相关的调试信息,或者只是使用WinDbg,并依照这篇博客文章中的步调来做。

最后,下面列出了所涉及的核心CLR源代码的不合部分:

    JIT Helpers for ‘PrecodeFixupThunk’

    PrecodeFixupThunk (i386 assembly)

    ThePreStub (i386 assembly)

    PreStubWorker(..)

    MethodDesc::DoPrestub(..)

    MethodDesc::DoBackpatch(..)

    MethodDesc::SetStableEntryPointInterlocked(..)

注意:这篇文章其实不是要研究JIT自己是如何工作的,如果感兴趣的话,可以参阅资深开发人员编写的概述。

JIT编译和执行引擎(EE)交互

使所有这些工作JIT和EE必须一起工作,以了解涉及的内容,查看这个评论,描述确定JIT编译可以使用哪种类型的预编码的规则。所有这些信息都存储在EE中,因为它是唯一一个完全了解某种体例的处所,所以JIT必须询问哪种模式可以工作。

另外,JIT必须向EE询问函数入口点的地址是什么,这是通过以下体例完成的:

· CEEInfo::getFunctionEntryPoint(..)

o Then calls MethodDesc::TryGetMultiCallableAddrOfCode(..)

· CEEInfo::getFunctionFixedEntryPoint(..)

o Then calls MethodDesc::GetMultiCallableAddrOfCode(..)

预编码和存根

有不合的类型或'precode'可用,'FIXUP','REMOTING'或'STUB',可以看到MethodDesc :: GetPrecodeType()中使用的规则。另外,由于它们是初级另外机制,所以它们在CPU体系结构中的实现与代码中的注释不合:

临时入口点有两个实现选项:

(1)紧凑的入口点。它们提供尽可能密集的入口点,但不克不及修补指向最终的代码。未经调试的体例的挪用是通过插槽进行间接挪用。

(2)预编码。预编码将被修补以指向最终的代码,从而可以将临时入口点嵌入到代码中。未被挪用的体例的挪用是直接挪用直接跳转。

(1)用于x86,(2)用于64位以在每个平台上获得最佳性能。对ARM(1)被使用。

BOTR还提供了更多关于“预编码”的信息。

最后,事实证明,如果没有遇到“stub”(或“trampolines”,“thunk”等),就不克不及进入CLR的内部,例如,

    虚拟体例(接口)调剂

    跳转存根

    泛型共享

    Dll导入回调

分层编译

在进一步讨论之前,要指出的是分层编译工作正在进行中。作为一个指示,为了让它工作,现在必须设置一个名为COMPLUS_EXPERIMENTAL_TieredCompilation的环境变量。看来,目前的工作重点放在基础设施上(即CLR的转变),那么在默认启用之前,必须进行年夜量的测试和性能阐发。

如果想了解该功能的目标以及它如何适应更广泛的“代码版本化”流程,那么建议阅读一些优秀的设计文档,包含未来的路线图可能性。

为了说明迄今为止所涉及的情况,目前正在进行的工作有:

    调试器(例如,如果在调试器连接之前,采取分层JIT编译重新编译该体例,并且在分层JIT编译替代代码时源线路断点停止工作,则断点不会被命中)

    阐发API - 例如分层JIT编译:实施额外的Profiler API

    诊断 - 全部通过分层JIT编译进行跟踪:设计/实施适当的诊断,将IL固定到ETW(Event Tracing for Windows)的本地映射

    Interpreter(解释器) - CLR有一个内置的解释器

ReJIT的历史

这是能够让CLR为用户重新调试的一个体例,可是它只能和Profiling API一起工作,这意味着用户必须编写一些C/ C ++ COM代码才能实现。另外ReJIT只允许在同一级别重新编译该体例,所以不会产生更多的优化代码。这主要是为了帮忙监视或阐发工具。

它是如何工作的?

最后是如何工作,这需要查看一些图表。首先回顾一下,一旦某个体例被JIT编译,关闭分层编译(与上面的图相同),其结果将是什么:
5095000220c3953ebce9

现在,作为比较,以下是启用分层编译的同一个阶段:
4e7f0002889210af30f0

主要区别在于分层编译迫使体例挪用通过另一个间接条理“预存”。这是为了能够计算体例被挪用的次数,然后一旦达到阈值(当前为30),“预存根”被重写为指向“优化本地代码”:

请注意,原始的“本地代码”仍然可用,所以如果需要,可以恢复更改,体例挪用可以返回到未优化的版本。

使用计数器

可以在prestub.cpp的这个评论中看到更多关于计数器的细节:
509500022134d171eeda

实质上,“存根”回调到“分层编译管理器”,直到“分层编译”被触发,一旦产生“存根”被“回补”,停止被挪用。

为什么没有“解释”模式?

如果人们想知道为什么分层编译没有解释模式,那么其谜底是已经采取一个解释器,可是这不适合生产代码吗?这是一个很好的问题,人们猜对了,因为解释器还不敷完善,不克不及运行生产代码。如果人们希望调试和阐发工具正常工作,只要有足够的时间和精力,这一切都是可以解决的,但这其实不是最容易开始的处所。

非优化和优化的JITting之间的开销有多年夜的不合?

在机器上,年夜约65%的时间使用了非优化的jitting,优化的jitting与IL输入年夜小类似,可是人们期望的结果会因工作负载和硬件而有所不合。进入第一步检查应该会更容易收集更好的丈量结果。

可是从几个月前开始,也许Mono的新.NET解释器会改变一些事情,谁知道呢?

为什么不采取LLVM?

最后,为什么不使用LLVM来编译代码,可以从Introduce a tiered JIT (comment)进行了解。

在GC(垃圾回收器)和EH(异常措置模型)中,CLR所需的LLVM支持与Java所需的LLVM支持(可能仍然存在)存在显著差别,并且必须在优化器中加以限制。仅举一个例子:CLR GC当前不克不及容忍指向对象末尾的托管指针。Java通过基类/派类生的成对述说机制措置这个问题。人们可能需要为这种成对的CLR述说提供支持,或者限制LLVM的优化器通过从不建立这些类型的指针。最重要的是,LLILC的JIT编译速度很慢,很难确定它最终会产生什么样的代码质量。

因此,弄清楚LLILC如何适应尚未存在的多层体例似乎为时尚早。现在的想法是将框架分层,并使用RyuJit作为二级JIT。随着人们越来越多的了解,可能会发现确实存在更高级另外工作空间,或者至少应该更好地理解需要做些什么才能使这些事情变得有意义。

总结

还有一个很好的副产品是微软的.NET Open Source 和开放的work-in-progress功能。对此感兴趣的人可以下载最新的代码测验考试一下,看看它们是如何工作的。


更多内容回复查看:
游客,如果您要查看本帖隐藏内容请回复
C VC C++ MFC 汇编 函数 脚本 辅助 多开 注入 内存 插件 破解 基址 窗口 大漠 绑定 编程 交流 论坛 实例 源码

14

主题

91

帖子

62

金币

团长

Rank: 10Rank: 10Rank: 10

积分
333

初来乍到新兵论坛好爱者

发表于 2017-12-21 18:38:59 | 显示全部楼层
你以为你讲的这么清楚,我就看得懂吗?你太天真了,能把我讲懂了,算你赢。[呲牙]
C VC C++ MFC 汇编 函数 脚本 辅助 多开 注入 内存 插件 破解 基址 窗口 大漠 绑定 编程 交流 论坛 实例 源码
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

豫ICP备14012807号-2|小黑屋|联系客服|金币冲值|VC中文网

GMT+8, 2022-1-23 09:55 , Processed in 0.218747 second(s), 26 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表