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

原文 : 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(),

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

  • 属性
  • 大部分的反射方法
  • 多维数组
  • 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 中讨论本文。


原文地址:https://www.cnblogs.com/chenug/p/8436819.html

时间: 2024-10-07 11:58:38

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

点击文本框弹出可供选择的checkbox复选框代码实例

点击文本框弹出可供选择的checkbox复选框代码实例:本章节分享一段代码实例,它能够点击文本框的时候,能够弹出下拉的checkbox复选框,选中复选框就能够将值写入文本框中,可能在实际应用中的效果没有这么直白简单,不过可以作为一个例子演示,以便于学习者理解和扩展.代码如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="author&qu

火车票预购系统社会因素可行性分析及其他供选择的方案

6.社会因素可行性分析 6.1法律因素 火车票订票系统是一个结局亿万中国人出行的系统,毫不夸张的说,它是一项关系着国计民生的重要环节,不仅为人们提供了快捷的服务,更重要的是可以在足不出户的情况下就能够解决购买火车票的问题,它使得中国的订票系统更加理性化也更加人性化,这也是在新时代的一种进步,能够使人们享受到现今科技带来的便利服务!且该项目为独立开发,在技术上没有使用任何现有的软件与方法.所以在法律方面不会存在侵犯专利权.侵犯版权等问题,完全按照合同规定的责任履行. 6.2用户使用可行性 使用本软

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

如今,“大型服务器”模式的时代已经过去,我们在运行一些大的Web应用时候,可以使用各种各样的负载均衡技术,这是一种更可行的方法,将使硬件成本降至最低. 过去当运行一个大的web应用时候意味着需要运行一个大型的web服务器.因为你的应用吸引了大量的用户,你将不得不在你的服务器里增加更多的内存和处理器.今天,“大型服务器”模式已经过去,取而代之的是大量的小服务器,使用各种各样的负载均衡技术. “更多小服务器”的优势超过过去的“大型服务器”模式体现在两个方面: 1. 如果服务器宕机,那么负载均衡系统将

iOS面试—3、runtime运行时

https://www.cnblogs.com/zhangxiaoping/p/5146647.html Objective-C的运行时参考 配套指南 Objective-C的运行时编程指南 在宣布 IONDRVLibraries.h NSObjCRuntime.h objc / message.h objc / objc-api.h objc / objc.h objc / runtime.h 概述 本文档介绍了OS X的Objective-C 2.0运行库支持的函数和数据结构.该功能是在发现

Unity3D脚本学习——运行时类

AssetBundle 类,继承自Object.AssetBundles让你通过WWW类流式加载额外的资源并在运行时实例化它们.AssetBundles通过BuildPipeline.BuildAssetBundle创建. 参见:WWW.assetBundle ,Loading Resources at Runtime ,BuildPipeline.BuildPlayer function Start () { var www = new WWW ("http://myserver/myBund

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

转载自infoQ 背景 InfoQ中文站此前报道过,2014年11月12日,ASP.NET之父.微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣布,微软将开源全部.NET核心运行时,并将.NET 扩展为可在 Linux 和 Mac OS 平台上运行..NET核心运行时将基于MIT开源许可协议发布,其中将包括执行.NET代码所需的一切项目——CLR.JIT编译器.垃圾收集器(GC)和核心.NET基础类库.此外,微软还发布了Visual Stu

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

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

【特种兵系列】编译时与运行时

在开发和设计的时候,我们需要考虑编译时,运行时以及构建时这三个概念.理解这几个概念可以更好地帮助你去了解一些基本的原理.下面是初学者晋级中级水平需要知道的一些问题. Q.下面的代码片段中,行A和行B所标识的代码有什么区别呢? public class ConstantFolding { static final int number1 = 5; static final int number2 = 6; static int number3 = 5; static int number4= 6;

Android运行时ART加载OAT文件的过程分析

在前面一文中,我们介绍了Android运行时ART,它的核心是OAT文件.OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容.这使得我们无需重新编译原有的APK就可以让它正常地在ART里面运行,也就是我们不需要改变原来的APK编程接口.本文我们通过OAT文件的加载过程分析OAT文件的结构,为后面分析ART的工作原理打基础. 老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注! OAT文件