Microsoft Visual C#2008减少加载的dll数

如何在Visual C#2008 Express Edition中调试时减少加载的dll数量?

在调试器中运行可视化C#项目时,由于2GB虚拟地址空间的碎片,我得到OutOfMemoryException,并且我们假设加载的dll可能是碎片的原因。

Brian Rasmussen,你结识了我的一天! 🙂

他的“禁用视觉工作室托管过程”的提议解决了这个问题。

(有关更多信息,请参阅下面的问题开发历史)


嗨,我需要两个大的int数组加载到内存中,每个都有大约1.2亿个元素(~470MB),并且都在一个Visual C#项目中。

当我试图实例化第二个数组时,我得到一个OutOfMemoryException。

我确实有足够的总可用内存,在进行网络搜索后,我认为我的问题是我的系统上没有足够大的连续空闲内存块。 但! – 当我只在一个Visual C#实例中实例化一个数组,然后打开另一个Visual C#实例时,第二个实例可以实例化一个470MB的数组。 (编辑说明:在上面的段落中,我的意思是在Visual C#的调试器中运行它)

任务管理器会像您期望的那样显示相应的内存使用量增加。 因此,整个系统上没有足够的连续内存块不是问题。 然后我尝试运行一个编译的可执行文件,实例化两个数组也工作(内存使用量1GB)

摘要:

Visual C#中的OutOfMemoryException使用两个大的int数组,但运行已编译的exe工作(mem使用1GB)和两个独立的Visual C#实例能够为我的大数组找到两个足够大的连续内存块,但我需要一个Visual C#实例来能够提供记忆。


更新:

首先要特别感谢nobugz和Brian Rasmussen,我认为他们的预测是“过程中2GB虚拟地址空间的碎片化”是问题所在。

按照他们的建议我使用VMMap和listdlls进行短暂的业余分析,我得到:
* 21个dll列出的“独立”-exe。 (工作和使用1GB内存的那个。)
*列出了vshost.exe版本的58个dll。 (调试时运行的版本,抛出exception,仅使用500MB)

VMMap向我展示了调试器版本最大的可用内存块为262,175,167,155,108MB。
所以VMMap说没有连续的500MB块,根据有关空闲块的信息,我添加了~9个较小的int-array,这些内存使用量增加了超过1,2GB,实际上确实有效。
所以我会说,我们可以称之为“2GB虚拟地址空间碎片”有罪。

从listdll-output我创建了一个小的电子表格,其中hex数转换为十进制以检查dll之间的空闲区域,我确实为独立版本(21)dll找到了大的可用空间,但没有找到vshost-debugger-version(58 dll文件)。 我并没有声称之间没有任何其他东西,我不确定我在那里做什么是有道理的,但它似乎与VMMaps分析一致,似乎单独的dll已经破坏了内存调试器版本。

因此,如果我能够减少调试器使用的dll数量,也许可以找到解决方案。
这可能吗? 2.如果是,我该怎么做?

第3次更新 :通过禁用Visual Studio托管过程(项目属性,调试),可以显着减少加载的DLL的数量。 这样做仍然允许您调试应用程序,但它将摆脱许多 DLL和一些帮助程序线程。

在一个小的测试项目中,当我禁用托管进程时,加载的DLL的数量从69增加到34。 我也摆脱了10多个线程。 总而言之,内存使用量的显着减少也应该有助于减少堆碎片。

有关托管过程的其他信息: http : //msdn.microsoft.com/en-us/library/ms242202.aspx


您可以在新应用程序中加载第二个arrays的原因是每个进程都获得一个完整的2 GB虚拟地址空间。 即OS将交换页面以允许每个进程处理总内存量。 当您尝试在一个进程中分配两个数组时,运行时必须能够分配两个所需大小的连续块。 你在数组中存储了什么? 如果存储对象,则每个对象都需要额外的空间。

请记住,应用程序实际上并不请求物理内存。 相反,每个应用程序都有一个地址空间,可以从中分配虚拟内存。 然后,OS将虚拟内存映射到物理内存。 这是一个相当复杂的过程(Russinovich花了100多页关于Windows如何处理Windows内部书中的内存)。 有关Windows如何执行此操作的详细信息,请参阅http://blogs.technet.com/markrussinovich/archive/2008/11/17/3155406.aspx

更新:我一直在思考这个问题,听起来有点奇怪。 通过Visual Studio运行应用程序时,您可能会看到根据您的配置加载的其他模块。 在我的设置中,由于分析器和TypeMock(通过探查器钩子实际上它的魔力),我在调试期间加载了许多不同的DLL。

根据这些的大小和加载地址,它们可能会阻止运行时分配连续的内存。 话虽如此,我仍然有点惊讶的是,在分配了两个大型arrays后,你得到一个OOM,因为它们的总大小小于1 GB。

您可以使用listdlls工具查看加载的DLL。 它会显示加载地址和大小。 或者,您可以使用WinDbg。 lm命令显示已加载的模块。 如果您还需要大小,则需要为详细输出指定v选项。 WinDbg还允许您检查.NET堆,这可以帮助您找出无法分配内存的原因。

第二次更新 :如果您使用的是Windows XP,则可以尝试重新绑定一些加载的DLL以释放更多的连续空间。 Vista和Windows 7使用ASLR ,所以我不确定你会从这些平台的变基上受益。

您正在与虚拟内存地址空间碎片作斗争。 32位版本的Windows上的进程有2千兆字节的可用内存。 该内存由代码和数据共享。 代码块是CLR和JIT编译器以及ngen-ed框架程序集。 数据块是.NET使用的各种堆,包括加载器堆(静态变量)和垃圾收集堆。 这些块位于存储器映射中的各种地址处。 可用内存可供您分配arrays。

问题是,大型数组需要连续的内存块。 地址空间中的“漏洞”(代码块和数据块之间)不够大,无法分配如此大的数组。 第一个洞通常在450到550兆字节之间,这就是你的第一个arrays分配成功的原因。 下一个可用的洞要小得多。 太小而不适合另一个大arrays,即使你有一个容易的千兆字节的可用内存,你也会获得OOM。

您可以使用SysInternals的VMMap实用程序查看进程的虚拟内存布局。 好的诊断,但它不会解决您的问题。 只有一个真正的修复,转移到64位版本的Windows。 也许更好:重新考虑您的算法,因此它不需要这么大的数组。

这本身不是答案,但也许替代方案可能有效。

如果问题确实是你的内存碎片,那么可能只有一个解决方法就是使用这些漏洞,而不是试图找到一个足够大的漏洞来连续。

这是一个非常简单的BigArray类,它不会增加太多开销(引入了一些开销,尤其是在构造函数中,以便初始化存储区)。

该数组的统计信息是:

  • 主要在404ms执行
  • static程序构造函数不会显示

该课程的统计数据是:

  • 主要花了473ms
  • static程序构造函数需要837ms(初始化存储桶)

该类分配了一堆8192个元素的数组(13位索引),它们在64位上用于引用类型将低于LOB限制。 如果你只是将它用于Int32,那么你可以将它提升到14甚至可能使它成为非通用的,尽管我怀疑它会提高性能。

在另一个方向,如果你担心你会有比8192元素arrays小的孔(64位64位或32位32KB),你可以减少比特大小桶通过其常量索引。 这将增加构造函数的开销,并增加更多的内存开销,因为最外层的数组会更大,但性能不会受到影响。

这是代码:

 using System; using NUnit.Framework; namespace ConsoleApplication5 { class Program { // static int[] a = new int[100 * 1024 * 1024]; static BigArray a = new BigArray(100 * 1024 * 1024); static void Main(string[] args) { int l = a.Length; for (int index = 0; index < l; index++) a[index] = index; for (int index = 0; index < l; index++) if (a[index] != index) throw new InvalidOperationException(); } } [TestFixture] public class BigArrayTests { [Test] public void Constructor_ZeroLength_ThrowsArgumentOutOfRangeException() { Assert.Throws(() => { new BigArray(0); }); } [Test] public void Constructor_NegativeLength_ThrowsArgumentOutOfRangeException() { Assert.Throws(() => { new BigArray(-1); }); } [Test] public void Indexer_SetsAndRetrievesCorrectValues() { BigArray array = new BigArray(10001); for (int index = 0; index < array.Length; index++) array[index] = index; for (int index = 0; index < array.Length; index++) Assert.That(array[index], Is.EqualTo(index)); } private const int PRIME_ARRAY_SIZE = 10007; [Test] public void Indexer_RetrieveElementJustPastEnd_ThrowsIndexOutOfRangeException() { BigArray array = new BigArray(PRIME_ARRAY_SIZE); Assert.Throws(() => { array[PRIME_ARRAY_SIZE] = 0; }); } [Test] public void Indexer_RetrieveElementJustBeforeStart_ThrowsIndexOutOfRangeException() { BigArray array = new BigArray(PRIME_ARRAY_SIZE); Assert.Throws(() => { array[-1] = 0; }); } [Test] public void Constructor_BoundarySizes_ProducesCorrectlySizedArrays() { for (int index = 1; index < 16384; index++) { BigArray arr = new BigArray(index); Assert.That(arr.Length, Is.EqualTo(index)); arr[index - 1] = 42; Assert.That(arr[index - 1], Is.EqualTo(42)); Assert.Throws(() => { arr[index] = 42; }); } } } public class BigArray { const int BUCKET_INDEX_BITS = 13; const int BUCKET_SIZE = 1 << BUCKET_INDEX_BITS; const int BUCKET_INDEX_MASK = BUCKET_SIZE - 1; private readonly T[][] _Buckets; private readonly int _Length; public BigArray(int length) { if (length < 1) throw new ArgumentOutOfRangeException("length"); _Length = length; int bucketCount = length >> BUCKET_INDEX_BITS; bool lastBucketIsFull = true; if ((length & BUCKET_INDEX_MASK) != 0) { bucketCount++; lastBucketIsFull = false; } _Buckets = new T[bucketCount][]; for (int index = 0; index < bucketCount; index++) { if (index < bucketCount - 1 || lastBucketIsFull) _Buckets[index] = new T[BUCKET_SIZE]; else _Buckets[index] = new T[(length & BUCKET_INDEX_MASK)]; } } public int Length { get { return _Length; } } public T this[int index] { get { return _Buckets[index >> BUCKET_INDEX_BITS][index & BUCKET_INDEX_MASK]; } set { _Buckets[index >> BUCKET_INDEX_BITS][index & BUCKET_INDEX_MASK] = value; } } } } 

我有一个类似的问题,我最后做的是使用列表而不是数组。 在创建列表时,我将容量设置为所需的大小,并在我尝试向它们添加值之前定义了两个列表。 我不确定你是否可以使用列表而不是数组,但它可能需要考虑。 最后我不得不在64位操作系统上运行可执行文件,因为当我将项目添加到列表时,总体内存使用量超过2GB,但至少我能够使用减少的数据集在本地运行和调试。

我有两个桌面应用程序和一个moble应用程序遇到内存不足限制的经验。 我理解这些问题。 我不知道您的要求,但我建议将查找数组移动到SQL CE中。 性能很好,您会感到惊讶,并且SQL CE正在进行中。 使用上一个桌面应用程序,我能够将内存占用量从2.1GB减少到720MB,这有利于通过显着减少页面错误来加速应用程序。 (你的问题是AppDomain的内存碎片,你无法控制它。)

老实说,在将这些数组压缩到内存后,我认为你不会对性能感到满意。 不要忘记,过多的页面错误会对性能产生重大影响。

如果你去SqlServerCe,请确保保持连接打开以提高性能。 此外,单行查找(标量)可能比返回结果集慢。

如果您真的想知道内存发生了什么,请使用CLR Profiler。 VMMap无济于事。 操作系统不会为您的应用程序分配内存。 该框架通过为自己抓取大量的操作系统内存(缓存内存)然后在需要时将这些内存分配给应用程序。

适用于.NET Framework 2.0的CLR Profiler, url为http://www.microsoft.com/downloads/details.aspx?familyid=A362781C-3870-43BE-8926-862B40AA0CD0&displaylang=en

一个问题:数组的所有元素都被占用了吗? 如果它们中的许多包含一些默认值,则可能使用稀疏数组的实现来减少内存消耗,该稀疏数组仅为非默认值分配内存。 只是一个想法。

每个32位进程都有2GB的地址空间(除非你要求用户在启动选项中添加/ 3GB),所以如果你能接受一些性能下降,你可以开始一个新的进程来获得2GB的地址空间 – 好吧,比这少一点。 所有CLR dll加上他们使用的所有Win32 DLL,新进程仍然是碎片化的,因此您可以通过使用本机语言(例如C ++)编写新进程来消除CLR dll导致的所有地址空间碎片。 您甚至可以将一些计算移动到新流程中,这样您就可以在主应用程序中获得更多的地址空间,而不会对主流程进行繁琐的处理。

您可以使用任何进程间通信方法在进程之间进行通信 。 您可以在一体化代码框架中找到许多IPC样本。