原文 : 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 中討論本文。