[搬运] DotNetAnywhere:可供选择的 .NET 运行时

原文 : 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

首先值得指出的是,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#

    更新:还有其余版本的各类错误修复和加强:数组

    源代码概览

    我以为 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.cJIT.c 中开始执行。在 JITit(..) 函数 的主入口中 "执行循环",其中最使人印象深入的是在一个 1,374 行代码的 switch 中就有 200 多个 case !!

    从更高的层面看,它所经历的整个过程以下所示:

    NET IL - DNA JIT Op-Codes

    与定义在 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),即便我再阅读完下面的文章仍是不得其解!! (有人能指教一下吗?)


    垃圾回收

    全部关于 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")。而后它必须查找每一个可能的引用,看看它是否真的指向 "对象的引用"。经过跟踪 平衡二叉搜索树 (按内存地址排序) 来执行操做, 流程以下所示:

    Binary Tree with Pointers into the Heap

    可是,这意味着全部的对象引用在分配时都必须存储在二叉树中,这会增长分配的开销。另外还须要额外的内存,每一个堆多占用 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机制的细节请看:

    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 Usage Explanation

    你能够经过下面的代码了解, ( 引用自 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  张蘅水 阅读( ...) 评论( ...) 编辑 收藏
相关文章
相关标签/搜索