原文 : DotNetAnywhere: An Alternative .NET Runtime 做者 : Matt Warren 译者 : 张很水javascript
我最近在收听一个名为DotNetRock 的优质播客,其中有以Knockout.js而闻名的Steven Sanderson 正在讨论 " WebAssembly And Blazor "。php
也许你还没听过,Blazor 正试图凭借WebAssembly的魔力将 .NET 带入到浏览器中。若是您想了解更多信息,Scott Hanselmen 已经在 " .NET和WebAssembly——这会是前端的将来吗? "一文中作了一番介绍。( 点击查看该文的翻译)。html
尽管 WebAssembly 很是酷炫,然而更让我感兴趣的是 Blazor 如何使用DotNetAnywhere做为底层的 .NET 运行时。本文将讨论DotNetAnywhere 是什么,能作什么,以及同完整的 .NET Framework 作比较。前端
首先值得指出的是,DotNetAnywhere (DNA) 被设计为一个彻底兼容的 .NET 运行时,能够运行被完整的.NET 框架编译的 dll 和 exe 。除此以外 (至少在理论上) 支持 如下的.NET 运行时的功能,真是使人激动!java
泛型 垃圾收集和析构 弱引用 完整的异常处理 - try/catch/finally PInvoke 接口 委托 事件 可空类型 一维数组 多线程
另外对于反射提供部分支持node
很是有限的只读方法 typeof(), GetType(), Type.Name, Type.Namespace, Type.IsEnum(), .ToString() 最后,还有一些目前不支持的功能:git 属性 大部分的反射方法 多维数组 Unsafe 代码 各类各样的错误或缺乏的功能可能会让代码没法在 DotNetAnywhere下运行,但其中一些已经被Blazor 修复,因此值得时不时检查 Blazor 的发布版本。github 现在,DotNetAnywhere 的原始仓库再也不活跃 (最后一个持续的活动是在2012年1月),因此将来任何的开发或错误修复均可能在 Blazor 的仓库中执行。若是你曾经在 DotNetAnywhere 中修复过某些东西,能够考虑在那里发一个PR。c# 更新:还有其余版本的各类错误修复和加强:数组 https://github.com/ncave/dotnet-js https://github.com/memsom/dna 源代码概览 我以为 DotNetAnywhere 运行时最使人印象深入的一点是 只由一我的开发,而且 只用了 40,000 行代码!反观,完整的 .NET 框架仅是垃圾收集器就有将近37000 行代码 ( 更多信息请我以前发布的CoreCLR 源代码漫游指南 )。 机器码 - 共 17,710 行 LOC File 3,164 JIT_Execute.c 1,778 JIT.c 1,109 PInvoke_CaseCode.h 630 Heap.c 618 MetaData.c 563 MetaDataTables.h 517 Type.c 491 MetaData_Fill.c 467 MetaData_Search.c 452 JIT_OpCodes.h 托管代码 - 共 28,783 行 LOC File 2393 corlib/System.Globalization/CalendricalCalculations.cs 2314 corlib/System/NumberFormatter.cs 1582 System.Drawing/System.Drawing/Pens.cs 1443 System.Drawing/System.Drawing/Brushes.cs 1405 System.Core/System.Linq/Enumerable.cs 745 corlib/System/DateTime.cs 693 corlib/System.IO/Path.cs 632 corlib/System.Collections.Generic/Dictionary.cs 598 corlib/System/String.cs 467 corlib/System.Text/StringBuilder.cs 关键组件 接下来,让咱们看一下 DotNetAnywhere 中的关键组件,正是咱们了解怎么兼容 .NET 运行时的好办法。一样咱们也能看到它与微软 .NET Framework 的差别。 加载 .NET dll DotNetAnywhere 所要作的第一件事就是加载、解析包含在 .dll 或者.exe 中的 元数据和代码。这一切都存放在MetaData.c中,主要是在LoadSingleTable(..) 函数中。经过添加一些调试代码,我可以从通常的 .NET dll 中获取全部类型的 元数据 摘要,这是一个很是有趣的列表: MetaData contains 1 Assemblies (MD_TABLE_ASSEMBLY) MetaData contains 1 Assembly References (MD_TABLE_ASSEMBLYREF) MetaData contains 0 Module References (MD_TABLE_MODULEREF) MetaData contains 40 Type References (MD_TABLE_TYPEREF) MetaData contains 13 Type Definitions (MD_TABLE_TYPEDEF) MetaData contains 14 Type Specifications (MD_TABLE_TYPESPEC) MetaData contains 5 Nested Classes (MD_TABLE_NESTEDCLASS) MetaData contains 11 Field Definitions (MD_TABLE_FIELDDEF) MetaData contains 0 Field RVA's (MD_TABLE_FIELDRVA) MetaData contains 2 Propeties (MD_TABLE_PROPERTY) MetaData contains 59 Member References (MD_TABLE_MEMBERREF) MetaData contains 2 Constants (MD_TABLE_CONSTANT) MetaData contains 35 Method Definitions (MD_TABLE_METHODDEF) MetaData contains 5 Method Specifications (MD_TABLE_METHODSPEC) MetaData contains 4 Method Semantics (MD_TABLE_PROPERTY) MetaData contains 0 Method Implementations (MD_TABLE_METHODIMPL) MetaData contains 22 Parameters (MD_TABLE_PARAM) MetaData contains 2 Interface Implementations (MD_TABLE_INTERFACEIMPL) MetaData contains 0 Implementation Maps? (MD_TABLE_IMPLMAP) MetaData contains 2 Generic Parameters (MD_TABLE_GENERICPARAM) MetaData contains 1 Generic Parameter Constraints (MD_TABLE_GENERICPARAMCONSTRAINT) MetaData contains 22 Custom Attributes (MD_TABLE_CUSTOMATTRIBUTE) MetaData contains 0 Security Info Items? (MD_TABLE_DECLSECURITY) 更多关于 元数据 的资料请参阅 介绍 CLR 元数据,解析.NET 程序集—–关于 PE 头文件 和 ECMA 标准 等文章。 执行 .NET IL DotNetAnywhere 的另外一大功能是 "即时编译器" (JIT),即执行 IL 的代码,从 JIT_Execute.c和JIT.c 中开始执行。在 JITit(..) 函数 的主入口中 "执行循环",其中最使人印象深入的是在一个 1,374 行代码的 switch 中就有 200 多个 case !! 从更高的层面看,它所经历的整个过程以下所示: 与定义在 CIL_OpCodes.h (CIL_XXX) .NET IL 操做码 ( Op-Codes) 不一样,DotNetAnywhere JIT 操做码 (Op-Codes) 是定义在 JIT_OpCodes.h (JIT_XXX)中。 有趣的是这部分 JIT 代码是 DotNetAnywhere 中惟一一处使用汇编编写 ,而且只是 win32 。 它容许使用 jump 或者 goto 在 C 源码中跳转标签,因此当 IL 指令被执行时,实际上并不会离开 JITit(..) 函数,控制(流程)只是从一处移动到别处,没必要进行完整的方法调用。 #ifdef __GNUC__ #define GET_LABEL(var, label) var = &&label #define GO_NEXT() goto **(void**)(pCurOp++) #else #ifdef WIN32 #define GET_LABEL(var, label) \ { __asm mov edi, label \ __asm mov var, edi } #define GO_NEXT() \ { __asm mov edi, pCurOp \ __asm add edi, 4 \ __asm mov pCurOp, edi \ __asm jmp DWORD PTR [edi - 4] } #endif IL 差别 在完整的 .NET framework 中,全部的 IL 代码在被 CPU 执行以前都是由 Just-in-Time Compiler (JIT) 转换为机器码。 如你所见, DotNetAnywhere "解释" (interprets) IL时是逐条执行指令,甚至会调用 JIT.c 文件来完成。 没有机器码 被反射发出 (emitted) ,因此这个命名仍是有点奇怪!? 或许这只是一个差别,但实在是没法让我搞清楚它是如何进行 "解释" (interpreting) 代码和 "即时编译" (JITting),即便我再阅读完下面的文章仍是不得其解!! (有人能指教一下吗?) 即时编译器和解释器有什么区别? 了解传统的解释器、JIT 编译器、JIT 解释器 和 AOT 编译器 的不一样之处 JIT vs Interpreters 为何咱们将 Java 字节码转换为机器码的东西称为 “JIT编译器” 而不是 “JIT解释器” ? 了解 JIT 编译和优化 垃圾回收 全部关于 DotNetAnywhere 的垃圾回收(GC) 代码都在 Heap.c 中,并且仍是 600 行易于阅读的代码。给你一个概览吧,下面是它暴露的函数列表: void Heap_Init(); void Heap_SetRoots(tHeapRoots *pHeapRoots, void *pRoots, U32 sizeInBytes); void Heap_UnmarkFinalizer(HEAP_PTR heapPtr); void Heap_GarbageCollect(); U32 Heap_NumCollections(); U32 Heap_GetTotalMemory(); HEAP_PTR Heap_Alloc(tMD_TypeDef *pTypeDef, U32 size); HEAP_PTR Heap_AllocType(tMD_TypeDef *pTypeDef); void Heap_MakeUndeletable(HEAP_PTR heapEntry); void Heap_MakeDeletable(HEAP_PTR heapEntry); tMD_TypeDef* Heap_GetType(HEAP_PTR heapEntry); HEAP_PTR Heap_Box(tMD_TypeDef *pType, PTR pMem); HEAP_PTR Heap_Clone(HEAP_PTR obj); U32 Heap_SyncTryEnter(HEAP_PTR obj); U32 Heap_SyncExit(HEAP_PTR obj); HEAP_PTR Heap_SetWeakRefTarget(HEAP_PTR target, HEAP_PTR weakRef); HEAP_PTR* Heap_GetWeakRefAddress(HEAP_PTR target); void Heap_RemovedWeakRefTarget(HEAP_PTR target); GC 差别 就像咱们对比 JIT/Interpreter 同样, 在 GC 上的差别一样可见。 Conservative GC 首先,DotNetAnywhere 的 GC 是 Conservative GC。简单地说,这意味着它不知道 (或者说确定) 内存的哪些区域是对象的引用/指针,仍是一个随机数 (看起来像内存地址)。而在.NET Framework 中 JIT 收集这些信息并存在GCInfo structure中,因此它的 GC 能够有效利用,而 DotNetAnywhere 是作不到。 相反, 在 标记(Mark) 的阶段,GC 获取全部可用的 " 根 (roots) ", 将一个对象中的全部内存地址视为 "潜在的" 引用(所以说它是 "conservative")。而后它必须查找每一个可能的引用,看看它是否真的指向 "对象的引用"。经过跟踪 平衡二叉搜索树 (按内存地址排序) 来执行操做, 流程以下所示: 可是,这意味着全部的对象引用在分配时都必须存储在二叉树中,这会增长分配的开销。另外还须要额外的内存,每一个堆多占用 20 个字节。咱们看看 tHeapEntry 的数据结构 (全部的指针占用 4 字节, U8 等于 1 字节,而 padding 可忽略不计), tHeapEntry *pLink[2] 是启用二叉树查找所需的额外数据。 struct tHeapEntry_ { // Left/right links in the heap binary tree tHeapEntry *pLink[2]; // The 'level' of this node. Leaf nodes have lowest level U8 level; // Used to mark that this node is still in use. // If this is set to 0xff, then this heap entry is undeletable. U8 marked; // Set to 1 if the Finalizer needs to be run. // Set to 2 if this has been added to the Finalizer queue // Set to 0 when the Finalizer has been run (or there is no Finalizer in the first place) // Only set on types that have a Finalizer U8 needToFinalize; // unused U8 padding; // The type in this heap entry tMD_TypeDef *pTypeDef; // Used for locking sync, and tracking WeakReference that point to this object tSync *pSync; // The user memory U8 memory[0]; }; 为何 DotNetAnywhere 这样作呢? DotNetAnywhere的做者Chris Bacon 是这样 解释: 告诉你吧,整个堆代码确实须要重写,减小每一个对象的内存开销,而且不须要分配二叉树。一开始设计 GC 时没有考虑那么多,(如今作的话)会增长不少代码。这是我一直想作的事情,但历来没有动手。为了尽快使用 GC 而只好如此。 在最初的设计中彻底没有 GC。它的速度很是快,以致于内存也会很快用完。 更多 "Conservative" 机制和 "Precise" GC机制的细节请看: Precise 对比 conservative 以及内部指针 .NET CLR 如何区分托管指针和非托管指针? GC 只作了 "标记-扫描", 不会作压缩 在 GC 方面另外一个不一样的行为是它不会在回收后作任何内存 压缩 ,正如 Steve Sanderson 在 working on Blazor 中所说: 在服务器端执行期间,咱们实际上并不须要任何内存固定 (pin),在客户端执行过程当中并无任何互操做,全部的东西(实际上)都是固定的。由于 DotNetAnywhere 的 GC只作标记扫描,没有任何压缩阶段。 此外,当一个对象被分配给 DotNetAnywhere 时,只是调用了 malloc(), 它的代码细节在 Heap_Alloc(..) 函数 中。因此它也没有"Generations" 或者 "Segments" 的概念,你在 .NET Framework GC 中见到的如 "Gen 0"、"Gen 1" 或者 "大对象堆" 等都不会出现。 线程模型 最后,咱们来看看线程模型,它与 .NET Framework 中的线程模型大相径庭。 线程差别 DotNetAnywhere (表面上)乐于为你建立线程并执行代码, 然而这只是一种幻觉. 事实上它只会跑在 一个线程 中, 不一样的线程之间 切换上下文: 你能够经过下面的代码了解, ( 引用自 Thread_Execute() 函数)将 numInst 设置为 100 并传入 JIT_Execute(..) 中: for (;;) { U32 minSleepTime = 0xffffffff; I32 threadExitValue; status = JIT_Execute(pThread, 100); switch (status) { .... } } 一个有趣的反作用是 DotNetAnywhere 中corlib 的实现代码将变得很是简单。如Interlocked.CompareExchange() 函数的内部实现 所示, 你所期待的同步就缺失了: tAsyncCall* System_Threading_Interlocked_CompareExchange_Int32( PTR pThis_, PTR pParams, PTR pReturnValue) { U32 *pLoc = INTERNALCALL_PARAM(0, U32*); U32 value = INTERNALCALL_PARAM(4, U32); U32 comparand = INTERNALCALL_PARAM(8, U32); *(U32*)pReturnValue = *pLoc; if (*pLoc == comparand) { *pLoc = value; } return NULL; } 基准对比 做为性能测试, 我将使用C# 最简版本 实现的 基于二叉树的计算机语言基准测试作对比。 注意:DotNetAnywhere 旨在运行于低内存设备,因此不意味着能与完整的 .NET Framework具备相同的性能。对比结果时切记!! .NET Framework, 4.6.1 - 0.36 seconds Invoked=TestApp.exe 15 stretch tree of depth 16 check: 131071 32768 trees of depth 4 check: 1015808 8192 trees of depth 6 check: 1040384 2048 trees of depth 8 check: 1046528 512 trees of depth 10 check: 1048064 128 trees of depth 12 check: 1048448 32 trees of depth 14 check: 1048544 long lived tree of depth 15 check: 65535 Exit code : 0 Elapsed time : 0.36 Kernel time : 0.06 (17.2%) User time : 0.16 (43.1%) page fault # : 6604 Working set : 25720 KB Paged pool : 187 KB Non-paged pool : 24 KB Page file size : 31160 KB DotNetAnywhere - 54.39 seconds Invoked=dna TestApp.exe 15 stretch tree of depth 16 check: 131071 32768 trees of depth 4 check: 1015808 8192 trees of depth 6 check: 1040384 2048 trees of depth 8 check: 1046528 512 trees of depth 10 check: 1048064 128 trees of depth 12 check: 1048448 32 trees of depth 14 check: 1048544 long lived tree of depth 15 check: 65535 Total execution time = 54288.33 ms Total GC time = 36857.03 ms Exit code : 0 Elapsed time : 54.39 Kernel time : 0.02 (0.0%) User time : 54.15 (99.6%) page fault # : 5699 Working set : 15548 KB Paged pool : 105 KB Non-paged pool : 8 KB Page file size : 13144 KB 显然,DotNetAnywhere 在这个基准测试中运行速度并不快(0.36秒/ 54秒)。然而,若是咱们对比另外一个基准测试,它的表现就好不少。DotNetAnywhere 在分配对象(类)时有很大的开销,而在使用结构时就不那么明显了。 Benchmark 1 (using classes) Benchmark 2 (using structs) Elapsed Time (secs) 3.1 2.0 GC Collections 96 67 Total GC time (msecs) 983.59 439.73 最后,我要感谢 Chris Bacon。DotNetAnywhere 真是一个伟大的代码库,对于咱们实现 .NET 运行时颇有帮助。 请在 Hacker News的 /r/programming 中讨论本文。 posted @ 2018-02-09 21:07 张蘅水 阅读( ...) 评论( ...) 编辑 收藏 刷新评论 刷新页面 返回顶部
最后,还有一些目前不支持的功能:git
属性 大部分的反射方法 多维数组 Unsafe 代码
各类各样的错误或缺乏的功能可能会让代码没法在 DotNetAnywhere下运行,但其中一些已经被Blazor 修复,因此值得时不时检查 Blazor 的发布版本。github
现在,DotNetAnywhere 的原始仓库再也不活跃 (最后一个持续的活动是在2012年1月),因此将来任何的开发或错误修复均可能在 Blazor 的仓库中执行。若是你曾经在 DotNetAnywhere 中修复过某些东西,能够考虑在那里发一个PR。c#
更新:还有其余版本的各类错误修复和加强:数组
我以为 DotNetAnywhere 运行时最使人印象深入的一点是 只由一我的开发,而且 只用了 40,000 行代码!反观,完整的 .NET 框架仅是垃圾收集器就有将近37000 行代码 ( 更多信息请我以前发布的CoreCLR 源代码漫游指南 )。
接下来,让咱们看一下 DotNetAnywhere 中的关键组件,正是咱们了解怎么兼容 .NET 运行时的好办法。一样咱们也能看到它与微软 .NET Framework 的差别。
DotNetAnywhere 所要作的第一件事就是加载、解析包含在 .dll 或者.exe 中的 元数据和代码。这一切都存放在MetaData.c中,主要是在LoadSingleTable(..) 函数中。经过添加一些调试代码,我可以从通常的 .NET dll 中获取全部类型的 元数据 摘要,这是一个很是有趣的列表:
MetaData contains 1 Assemblies (MD_TABLE_ASSEMBLY) MetaData contains 1 Assembly References (MD_TABLE_ASSEMBLYREF) MetaData contains 0 Module References (MD_TABLE_MODULEREF) MetaData contains 40 Type References (MD_TABLE_TYPEREF) MetaData contains 13 Type Definitions (MD_TABLE_TYPEDEF) MetaData contains 14 Type Specifications (MD_TABLE_TYPESPEC) MetaData contains 5 Nested Classes (MD_TABLE_NESTEDCLASS) MetaData contains 11 Field Definitions (MD_TABLE_FIELDDEF) MetaData contains 0 Field RVA's (MD_TABLE_FIELDRVA) MetaData contains 2 Propeties (MD_TABLE_PROPERTY) MetaData contains 59 Member References (MD_TABLE_MEMBERREF) MetaData contains 2 Constants (MD_TABLE_CONSTANT) MetaData contains 35 Method Definitions (MD_TABLE_METHODDEF) MetaData contains 5 Method Specifications (MD_TABLE_METHODSPEC) MetaData contains 4 Method Semantics (MD_TABLE_PROPERTY) MetaData contains 0 Method Implementations (MD_TABLE_METHODIMPL) MetaData contains 22 Parameters (MD_TABLE_PARAM) MetaData contains 2 Interface Implementations (MD_TABLE_INTERFACEIMPL) MetaData contains 0 Implementation Maps? (MD_TABLE_IMPLMAP) MetaData contains 2 Generic Parameters (MD_TABLE_GENERICPARAM) MetaData contains 1 Generic Parameter Constraints (MD_TABLE_GENERICPARAMCONSTRAINT) MetaData contains 22 Custom Attributes (MD_TABLE_CUSTOMATTRIBUTE) MetaData contains 0 Security Info Items? (MD_TABLE_DECLSECURITY)
更多关于 元数据 的资料请参阅 介绍 CLR 元数据,解析.NET 程序集—–关于 PE 头文件 和 ECMA 标准 等文章。
DotNetAnywhere 的另外一大功能是 "即时编译器" (JIT),即执行 IL 的代码,从 JIT_Execute.c和JIT.c 中开始执行。在 JITit(..) 函数 的主入口中 "执行循环",其中最使人印象深入的是在一个 1,374 行代码的 switch 中就有 200 多个 case !!
switch
case
从更高的层面看,它所经历的整个过程以下所示:
与定义在 CIL_OpCodes.h (CIL_XXX) .NET IL 操做码 ( Op-Codes) 不一样,DotNetAnywhere JIT 操做码 (Op-Codes) 是定义在 JIT_OpCodes.h (JIT_XXX)中。
CIL_XXX
JIT_XXX
有趣的是这部分 JIT 代码是 DotNetAnywhere 中惟一一处使用汇编编写 ,而且只是 win32 。 它容许使用 jump 或者 goto 在 C 源码中跳转标签,因此当 IL 指令被执行时,实际上并不会离开 JITit(..) 函数,控制(流程)只是从一处移动到别处,没必要进行完整的方法调用。
win32
jump
goto
JITit(..)
#ifdef __GNUC__ #define GET_LABEL(var, label) var = &&label #define GO_NEXT() goto **(void**)(pCurOp++) #else #ifdef WIN32 #define GET_LABEL(var, label) \ { __asm mov edi, label \ __asm mov var, edi } #define GO_NEXT() \ { __asm mov edi, pCurOp \ __asm add edi, 4 \ __asm mov pCurOp, edi \ __asm jmp DWORD PTR [edi - 4] } #endif
在完整的 .NET framework 中,全部的 IL 代码在被 CPU 执行以前都是由 Just-in-Time Compiler (JIT) 转换为机器码。
如你所见, DotNetAnywhere "解释" (interprets) IL时是逐条执行指令,甚至会调用 JIT.c 文件来完成。 没有机器码 被反射发出 (emitted) ,因此这个命名仍是有点奇怪!?
或许这只是一个差别,但实在是没法让我搞清楚它是如何进行 "解释" (interpreting) 代码和 "即时编译" (JITting),即便我再阅读完下面的文章仍是不得其解!! (有人能指教一下吗?)
全部关于 DotNetAnywhere 的垃圾回收(GC) 代码都在 Heap.c 中,并且仍是 600 行易于阅读的代码。给你一个概览吧,下面是它暴露的函数列表:
void Heap_Init(); void Heap_SetRoots(tHeapRoots *pHeapRoots, void *pRoots, U32 sizeInBytes); void Heap_UnmarkFinalizer(HEAP_PTR heapPtr); void Heap_GarbageCollect(); U32 Heap_NumCollections(); U32 Heap_GetTotalMemory(); HEAP_PTR Heap_Alloc(tMD_TypeDef *pTypeDef, U32 size); HEAP_PTR Heap_AllocType(tMD_TypeDef *pTypeDef); void Heap_MakeUndeletable(HEAP_PTR heapEntry); void Heap_MakeDeletable(HEAP_PTR heapEntry); tMD_TypeDef* Heap_GetType(HEAP_PTR heapEntry); HEAP_PTR Heap_Box(tMD_TypeDef *pType, PTR pMem); HEAP_PTR Heap_Clone(HEAP_PTR obj); U32 Heap_SyncTryEnter(HEAP_PTR obj); U32 Heap_SyncExit(HEAP_PTR obj); HEAP_PTR Heap_SetWeakRefTarget(HEAP_PTR target, HEAP_PTR weakRef); HEAP_PTR* Heap_GetWeakRefAddress(HEAP_PTR target); void Heap_RemovedWeakRefTarget(HEAP_PTR target);
就像咱们对比 JIT/Interpreter 同样, 在 GC 上的差别一样可见。
首先,DotNetAnywhere 的 GC 是 Conservative GC。简单地说,这意味着它不知道 (或者说确定) 内存的哪些区域是对象的引用/指针,仍是一个随机数 (看起来像内存地址)。而在.NET Framework 中 JIT 收集这些信息并存在GCInfo structure中,因此它的 GC 能够有效利用,而 DotNetAnywhere 是作不到。
相反, 在 标记(Mark) 的阶段,GC 获取全部可用的 " 根 (roots) ", 将一个对象中的全部内存地址视为 "潜在的" 引用(所以说它是 "conservative")。而后它必须查找每一个可能的引用,看看它是否真的指向 "对象的引用"。经过跟踪 平衡二叉搜索树 (按内存地址排序) 来执行操做, 流程以下所示:
标记(Mark)
可是,这意味着全部的对象引用在分配时都必须存储在二叉树中,这会增长分配的开销。另外还须要额外的内存,每一个堆多占用 20 个字节。咱们看看 tHeapEntry 的数据结构 (全部的指针占用 4 字节, U8 等于 1 字节,而 padding 可忽略不计), tHeapEntry *pLink[2] 是启用二叉树查找所需的额外数据。
tHeapEntry
U8
padding
tHeapEntry *pLink[2]
struct tHeapEntry_ { // Left/right links in the heap binary tree tHeapEntry *pLink[2]; // The 'level' of this node. Leaf nodes have lowest level U8 level; // Used to mark that this node is still in use. // If this is set to 0xff, then this heap entry is undeletable. U8 marked; // Set to 1 if the Finalizer needs to be run. // Set to 2 if this has been added to the Finalizer queue // Set to 0 when the Finalizer has been run (or there is no Finalizer in the first place) // Only set on types that have a Finalizer U8 needToFinalize; // unused U8 padding; // The type in this heap entry tMD_TypeDef *pTypeDef; // Used for locking sync, and tracking WeakReference that point to this object tSync *pSync; // The user memory U8 memory[0]; };
为何 DotNetAnywhere 这样作呢? DotNetAnywhere的做者Chris Bacon 是这样 解释:
告诉你吧,整个堆代码确实须要重写,减小每一个对象的内存开销,而且不须要分配二叉树。一开始设计 GC 时没有考虑那么多,(如今作的话)会增长不少代码。这是我一直想作的事情,但历来没有动手。为了尽快使用 GC 而只好如此。 在最初的设计中彻底没有 GC。它的速度很是快,以致于内存也会很快用完。
更多 "Conservative" 机制和 "Precise" GC机制的细节请看:
在 GC 方面另外一个不一样的行为是它不会在回收后作任何内存 压缩 ,正如 Steve Sanderson 在 working on Blazor 中所说:
在服务器端执行期间,咱们实际上并不须要任何内存固定 (pin),在客户端执行过程当中并无任何互操做,全部的东西(实际上)都是固定的。由于 DotNetAnywhere 的 GC只作标记扫描,没有任何压缩阶段。
此外,当一个对象被分配给 DotNetAnywhere 时,只是调用了 malloc(), 它的代码细节在 Heap_Alloc(..) 函数 中。因此它也没有"Generations" 或者 "Segments" 的概念,你在 .NET Framework GC 中见到的如 "Gen 0"、"Gen 1" 或者 "大对象堆" 等都不会出现。
最后,咱们来看看线程模型,它与 .NET Framework 中的线程模型大相径庭。
DotNetAnywhere (表面上)乐于为你建立线程并执行代码, 然而这只是一种幻觉. 事实上它只会跑在 一个线程 中, 不一样的线程之间 切换上下文:
你能够经过下面的代码了解, ( 引用自 Thread_Execute() 函数)将 numInst 设置为 100 并传入 JIT_Execute(..) 中:
numInst
100
JIT_Execute(..)
for (;;) { U32 minSleepTime = 0xffffffff; I32 threadExitValue; status = JIT_Execute(pThread, 100); switch (status) { .... } }
一个有趣的反作用是 DotNetAnywhere 中corlib 的实现代码将变得很是简单。如Interlocked.CompareExchange() 函数的内部实现 所示, 你所期待的同步就缺失了:
corlib
Interlocked.CompareExchange()
tAsyncCall* System_Threading_Interlocked_CompareExchange_Int32( PTR pThis_, PTR pParams, PTR pReturnValue) { U32 *pLoc = INTERNALCALL_PARAM(0, U32*); U32 value = INTERNALCALL_PARAM(4, U32); U32 comparand = INTERNALCALL_PARAM(8, U32); *(U32*)pReturnValue = *pLoc; if (*pLoc == comparand) { *pLoc = value; } return NULL; }
做为性能测试, 我将使用C# 最简版本 实现的 基于二叉树的计算机语言基准测试作对比。
注意:DotNetAnywhere 旨在运行于低内存设备,因此不意味着能与完整的 .NET Framework具备相同的性能。对比结果时切记!!
Invoked=TestApp.exe 15 stretch tree of depth 16 check: 131071 32768 trees of depth 4 check: 1015808 8192 trees of depth 6 check: 1040384 2048 trees of depth 8 check: 1046528 512 trees of depth 10 check: 1048064 128 trees of depth 12 check: 1048448 32 trees of depth 14 check: 1048544 long lived tree of depth 15 check: 65535 Exit code : 0 Elapsed time : 0.36 Kernel time : 0.06 (17.2%) User time : 0.16 (43.1%) page fault # : 6604 Working set : 25720 KB Paged pool : 187 KB Non-paged pool : 24 KB Page file size : 31160 KB
Invoked=dna TestApp.exe 15 stretch tree of depth 16 check: 131071 32768 trees of depth 4 check: 1015808 8192 trees of depth 6 check: 1040384 2048 trees of depth 8 check: 1046528 512 trees of depth 10 check: 1048064 128 trees of depth 12 check: 1048448 32 trees of depth 14 check: 1048544 long lived tree of depth 15 check: 65535 Total execution time = 54288.33 ms Total GC time = 36857.03 ms Exit code : 0 Elapsed time : 54.39 Kernel time : 0.02 (0.0%) User time : 54.15 (99.6%) page fault # : 5699 Working set : 15548 KB Paged pool : 105 KB Non-paged pool : 8 KB Page file size : 13144 KB
显然,DotNetAnywhere 在这个基准测试中运行速度并不快(0.36秒/ 54秒)。然而,若是咱们对比另外一个基准测试,它的表现就好不少。DotNetAnywhere 在分配对象(类)时有很大的开销,而在使用结构时就不那么明显了。
类
结构
classes
structs
最后,我要感谢 Chris Bacon。DotNetAnywhere 真是一个伟大的代码库,对于咱们实现 .NET 运行时颇有帮助。
请在 Hacker News的 /r/programming 中讨论本文。