收集动态程序集:从一个程序集移动到多个模块和程序集,在没有 RunAndCollect 的情况下内存使用量增加了三倍

Collecting Dynamic Assemblies: Moving from one assembly to multiple modules and assemblies has tripled memory usage without RunAndCollect

在我们公司,我们有多种内部语言和 compilers/decompilers,因此我们积极利用 C# 动态代码生成和反射功能。我们编写的大部分代码只是在 运行 时间或使用我们专有的脚本语言动态创建代码。

在某一时刻,我们发现向动态程序集添加类型会扩展 O(n^2),大概是因为整个程序集都被重写了吗?这意味着当创建超过 10k 种类型时,我们的软件可能会变慢。我们当时所做的是保留一个动态程序集和模块,并在我们生成动态代码时向其添加类型(DynamicMethods 有一些例外)。

为了解决这个问题,我们对类型、模块和程序集的最佳组合进行了梯度下降以动态生成,以便我们可以获得 O(n) 性能。

我们实现了一个抽象层,当我们创建动态类型时,它会根据结果显示的最佳方式根据需要自动创建 modules/assemblies。这具有所需的性能优势。这些 assemblies/modules/types 反射对象 总是 通过各种字典和哈希集在幕后保持对它们的强烈引用。

然而,几个月后,一些非常奇怪的问题开始出现。第一个非常断断续续,每 3/4 运行 秒发生一次。这是致命的 ExecutionEngineException,错误代码 80131506。这非常令人费解,我们最初认为这是从 .NET 451 升级到 .NET 472 引起的(我们 非常 以前偶尔遇到过这个问题) .最终我们追踪到将 System.Reflection.Emit.AssemblyBuilderAccess 设置为 运行AndCollect。当我们将其更改为 运行 时,我们再也没有遇到过这个问题。请记住,我们始终保持对这些程序集的强引用,因此对于如何解决问题非常困惑。

然而,问题并没有就此停止,当我们进一步深入 .NET 的深渊时,我们发现了无法形容的恐怖。看起来我们程序的内存使用量增加了两倍,而且许多程序都因内存不足异常而失败。通过限制我们程序的 'Strength'(减少内存使用的设置),我们的程序将 运行,但花费的时间是原来的三倍。

鉴于此行为,似乎我们生成了太多动态程序集,需要收集它们,否则我们 运行 内存不足。但是,我发现很难相信动态程序集会用掉很多 space,而且我不明白如何收集它们,因为我们维护的对象是动态程序集中类型的实例(大概持有对 Type 对象的引用 ) 并直接对这些动态程序集的反射对象进行强引用。

所以我的问题是动态程序集集合在 .NET Framework 中究竟是如何工作的,为什么我们会收到这些 FatalExecutionEngine Exceptions/OutOfMemory 异常?

我认为您收到的致命错误代码 (80135106) 通常是由动态程序集中的循环类型引用引起的(因此程序集 1 包含引用程序集 2 中类型的类型 - 可能通过私有字段,但程序集2 最终包含引用程序集 1) 的类型。收集程序集时很容易造成堆栈溢出。

请参阅此 link 以了解通过 XAML Visual Studio 2008 crashes when displaying XAML view. How to get more information?

发生的实例