[搬運] 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  張蘅水 閱讀( ...) 評論( ...) 編輯 收藏
相關文章
相關標籤/搜索