[搬运] 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(), .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 行
关键组件
接下来,让我们看一下 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机制的细节请看:
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 运行时的更多相关文章
- .Net Core vs .Net Framework 如何为一个应用程序选择一个运行时
.Net Core是下一件大事吗?我已经使用了一段时间了,我倾向认为它是.事实上,我们推测,在2018年,对这项技术熟练的开发人员将会有巨大的需求.但是它和.Net Framework的区别是什么?你 ...
- 大型Web应用运行时 PHP负载均衡指南
如今,“大型服务器”模式的时代已经过去,我们在运行一些大的Web应用时候,可以使用各种各样的负载均衡技术,这是一种更可行的方法,将使硬件成本降至最低. 过去当运行一个大的web应用时候意味着需要运行一 ...
- 乘风破浪,.Net Core遇见Dapr,为云原生而生的分布式应用运行时
Dapr是一个由微软主导的云原生开源项目,国内云计算巨头阿里云也积极参与其中,2019年10月首次发布,到今年2月正式发布V1.0版本.在不到一年半的时间内,github star数达到了1.2万,超 ...
- iOS 运行时runtime控制私有变量以及私有方法
OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtime机制让我们可以在程序运行时动态修改类.对象中的所有属性.方法,就算是私有方法以及私有属性都是可以动 ...
- 【原】iOS动态性(二):运行时runtime初探(强制获取并修改私有变量,强制增加及修改私有方法等)
OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtime机制让我们可以在程序运行时动态修改类.对象中的所有属性.方法,就算是私有方法以及私有属性都是可以动 ...
- Java面试题问与答——编译时与运行时
在开发和设计的时候,我们需要考虑编译时,运行时以及构建时这三个概念.理解这几个概念可以更好地帮助你去了解一些基本的原理.下面是初学者晋级中级水平需要知道的一些问题. Q.下面的代码片段中,行A和行B所 ...
- arcgis安装msi安装包提示"在未标记为正在运行时,调用了RunScript”解决办法
安装msi安装包提示"在未标记为正在运行时,调用了RunScript”解决办法 windows/temp目录相关权限不对,右击temp文件夹,选择管理员获取所有权限.
- 《转》.NET开源核心运行时,且行且珍惜
转载自infoQ 背景 InfoQ中文站此前报道过,2014年11月12日,ASP.NET之父.微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣 ...
- visual studio运行时库MT、MTd、MD、MDd的研究(转载)
转载:http://blog.csdn.net/ybxuwei/article/details/9095067 转载:http://blog.sina.com.cn/s/blog_624485f701 ...
随机推荐
- Wannafly模拟赛 A.矩阵(二分答案+hash)
矩阵 时间限制:1秒 空间限制:131072K 题目描述 给出一个n * m的矩阵.让你从中发现一个最大的正方形.使得这样子的正方形在矩阵中出现了至少两次.输出最大正方形的边长. 输入描述: 第一行两 ...
- vijos 1213:80人环游世界
描述 想必大家都看过成龙大哥的<80天环游世界>,里面的紧张刺激的打斗场面一定给你留下了深刻的印象.现在就有这么一个80人的团伙,也想来一次环游世界. 他们打算兵分多路,游遍每一个国家. ...
- c++(八皇后)
八皇后是一道很具典型性的题目.它的基本要求是这样的:在一个8*8的矩阵上面放置8个物体,一个矩阵点只允许放置一个物体,任意两个点不能在一行上,也不能在一列上,不能在一条左斜线上,当然也不能在一条右斜线 ...
- linux日志查看命令
tail tail 命令用于显示文本文件的末尾几行, 对于监控文件日志特别有用 tail example.txt #显示文件 example.txt 的后十行内容: tail -n 20 exampl ...
- 关于jquery ajax跨域请求获取response headers问题
背景:最近项目jwt用户认证方式,关于jwt本人就不再赘述,大家可自行百度. jwt token基本流程是这样的: 用户使用用户名密码来请求服务器 服务器进行验证用户的信息 服务器通过验证发送给用户一 ...
- 【转载备忘】PowerDesigner16.5基本使用
这两天都在设计数据库,使用了powerdesigner进行设计的,然后摸索了好久,本来打算写一篇文章来记述一下的,写了一半,突然发现网上早就有比我写的好的文章了,所有删了之前写的,直接贴出来那个文章的 ...
- sha1() 函数
sha1() 函数计算字符串的 SHA-1 散列. sha1() 函数使用美国 Secure Hash 算法 1. 来自 RFC 3174 的解释 - 美国 Secure Hash 算法 1:SHA- ...
- 7系列高速收发器总结 GTP IP核使用篇
上一篇7系列收发器博文讲解了GTP IP核的基本配置,本文继续分析如何将它使用起来.生成IP核后打开example design,先看看工程中包含的文件结构. 顶层文件下包含了gtp ip核系统顶层文 ...
- hibernate.dialect是干嘛用的?
dialect[ˈdaɪəlekt]就是“方言”,因为hibernate是要把Java对象转换成关系数据库来描述的,而关系数据库虽然有一些统一的标准,如SQL-92等,但是实际上各数据库如Oracle ...
- JXLS使用方法(文件上传读取)xlsx文件读取
1.官方文档:http://jxls.sourceforge.net/reference/reader.html 2.demo git地址:https://bitbucket.org/leonate/ ...