原文 : DotNetAnywhere: An Alternative .NET Runtime

作者 : Matt Warren

译者 : 张很水

我最近在收听一个名为DotNetRock 的优质播客,其中有以Knockout.js而闻名的Steven Sanderson 正在讨论 " WebAssembly And Blazor "

也许你还没听过,Blazor 正试图凭借WebAssembly的魔力将 .NET 带入到浏览器中。如果您想了解更多信息,Scott Hanselmen 已经在 " .NET和WebAssembly——这会是前端的未来吗? "一文中做了一番介绍。( 点击查看该文的翻译)。

尽管 WebAssembly 非常酷炫,然而更让我感兴趣的是 Blazor 如何使用DotNetAnywhere作为底层的 .NET 运行时。本文将讨论DotNetAnywhere 是什么,能做什么,以及同完整的 .NET Framework 做比较。


DotNetAnywhere

首先值得指出的是,DotNetAnywhere (DNA) 被设计为一个完全兼容的 .NET 运行时,可以运行被完整的.NET 框架编译的 dll 和 exe 。除此之外 (至少在理论上) 支持 以下的.NET 运行时的功能,真是令人激动!

  • 泛型
  • 垃圾收集和析构
  • 弱引用
  • 完整的异常处理 - try/catch/finally
  • PInvoke
  • 接口
  • 委托
  • 事件
  • 可空类型
  • 一维数组
  • 多线程

另外对于反射提供部分支持

  • 非常有限的只读方法

    typeof(), GetType(), Type.Name, Type.Namespace, Type.IsEnum(), .ToString()

最后,还有一些目前不支持的功能:

  • 属性
  • 大部分的反射方法
  • 多维数组
  • Unsafe 代码

各种各样的错误或缺少的功能可能会让代码无法在 DotNetAnywhere下运行,但其中一些已经被Blazor 修复,所以值得时不时检查 Blazor 的发布版本。

如今,DotNetAnywhere 的原始仓库不再活跃 (最后一个持续的活动是在2012年1月),所以未来任何的开发或错误修复都可能在 Blazor 的仓库中执行。如果你曾经在 DotNetAnywhere 中修复过某些东西,可以考虑在那里发一个PR。

更新:还有其他版本的各种错误修复和增强:

源代码概览

我觉得 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 !!

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

与定义在 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")。然后它必须查找每个可能的引用,看看它是否真的指向 "对象的引用"。通过跟踪 平衡二叉搜索树 (按内存地址排序) 来执行操作, 流程如下所示:

但是,这意味着所有的对象引用在分配时都必须存储在二叉树中,这会增加分配的开销。另外还需要额外的内存,每个堆多占用 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_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 中讨论本文。


[搬运] DotNetAnywhere:可供选择的 .NET 运行时的更多相关文章

  1. .Net Core vs .Net Framework 如何为一个应用程序选择一个运行时

    .Net Core是下一件大事吗?我已经使用了一段时间了,我倾向认为它是.事实上,我们推测,在2018年,对这项技术熟练的开发人员将会有巨大的需求.但是它和.Net Framework的区别是什么?你 ...

  2. 大型Web应用运行时 PHP负载均衡指南

    如今,“大型服务器”模式的时代已经过去,我们在运行一些大的Web应用时候,可以使用各种各样的负载均衡技术,这是一种更可行的方法,将使硬件成本降至最低. 过去当运行一个大的web应用时候意味着需要运行一 ...

  3. 乘风破浪,.Net Core遇见Dapr,为云原生而生的分布式应用运行时

    Dapr是一个由微软主导的云原生开源项目,国内云计算巨头阿里云也积极参与其中,2019年10月首次发布,到今年2月正式发布V1.0版本.在不到一年半的时间内,github star数达到了1.2万,超 ...

  4. iOS 运行时runtime控制私有变量以及私有方法

    OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtime机制让我们可以在程序运行时动态修改类.对象中的所有属性.方法,就算是私有方法以及私有属性都是可以动 ...

  5. 【原】iOS动态性(二):运行时runtime初探(强制获取并修改私有变量,强制增加及修改私有方法等)

    OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtime机制让我们可以在程序运行时动态修改类.对象中的所有属性.方法,就算是私有方法以及私有属性都是可以动 ...

  6. Java面试题问与答——编译时与运行时

    在开发和设计的时候,我们需要考虑编译时,运行时以及构建时这三个概念.理解这几个概念可以更好地帮助你去了解一些基本的原理.下面是初学者晋级中级水平需要知道的一些问题. Q.下面的代码片段中,行A和行B所 ...

  7. arcgis安装msi安装包提示"在未标记为正在运行时,调用了RunScript”解决办法

    安装msi安装包提示"在未标记为正在运行时,调用了RunScript”解决办法   windows/temp目录相关权限不对,右击temp文件夹,选择管理员获取所有权限.

  8. 《转》.NET开源核心运行时,且行且珍惜

    转载自infoQ 背景 InfoQ中文站此前报道过,2014年11月12日,ASP.NET之父.微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣 ...

  9. visual studio运行时库MT、MTd、MD、MDd的研究(转载)

    转载:http://blog.csdn.net/ybxuwei/article/details/9095067 转载:http://blog.sina.com.cn/s/blog_624485f701 ...

随机推荐

  1. [51nod1614]刷题计划

    大赛将至,摆在你面前的是n道题目,第 i(1 ≤ i ≤ n) 道题目能提升 ai 点智力值,代码量为 bi KB,无聊值为 ci ,求至少提升m点智力值的情况下,所做题目代码量之和*无聊值之和最小为 ...

  2. Vijos P1066 弱弱的战壕【多解,线段树,暴力,树状数组】

    弱弱的战壕 描述 永恒和mx正在玩一个即时战略游戏,名字嘛~~~~~~恕本人记性不好,忘了-_-b. mx在他的基地附近建立了n个战壕,每个战壕都是一个独立的作战单位,射程可以达到无限(“mx不赢定了 ...

  3. A. Duff and Meat

    A. Duff and Meat time limit per test 1 second memory limit per test 256 megabytes input standard inp ...

  4. android狼人杀源码,桌面源码,猎豹快切源码

    Android精选源码 android实现狼人杀app源码 android实现精心打造的Android基础框架源码 android热门电影的客户端源码 android 实现桌面的Launcher源码 ...

  5. c++(查找)

    无论是数据库,还是普通的ERP系统,查找功能数据处理的一个基本功能.数据查找并不复杂,但是如何实现数据又快又好地查找呢?前人在实践中积累的一些方法,值得我们好好学些一下.我们假定查找的数据唯一存在,数 ...

  6. 蓝桥杯 0/1背包问题 (java)

      今天第一次接触了0/1背包问题,总结一下,方便以后修改.不对的地方还请大家不啬赐教! 上一个蓝桥杯的例题: 数据规模和约定 代码: import java.util.Scanner; public ...

  7. [国嵌笔记][006][Linux文本编辑器]

    Linux中常见的文本编辑器有Vi和Emacs Vim有3中工作模式:命令行模式.插入模式.底行模式 1.键入i进入插入模式 2.键入[Esc]退回到命令行模式 3.键入:进入底行模式,再键入wq保存 ...

  8. 用于 C♯ 图像识别的轮廓分析技术

    用于 C♯ 图像识别的轮廓分析技术 供稿:Conmajia 标题:Contour Analysis for Image Recognition in C# 作者:Pavel Torgashov 此中文 ...

  9. 一致性哈希java实现

    值得注意的点 哈希函数的选择 murmur哈希函数 该函数是非加密型哈希,性能高,且发生哈希碰撞的概率据说很低 md5 SHA 可以选择guava包,提供了丰富的哈希函数的API 支持虚拟节点+加权, ...

  10. CSS学习笔记day1

    1.css的简介  css:层叠样式表 (层叠:一层一层的:样式表:很多的属性和属性值) 使页面显示效果更好 将页面内容和显示样式进行分离,提高了显示功能. 2.css和html的结合方式(4种) 在 ...