微软Visual C# 2008减少加载的DLL数量

16

如何在使用Visual C# 2008 Express Edition进行调试时减少已加载的dll数量?

当在调试器中运行Visual C#项目时,由于2GB虚拟地址空间的碎片化,我会遇到OutOfMemoryException异常,我们认为已加载的dll可能是碎片化的原因。

Brian Rasmussen,你让我一天都很开心! :)

他提出的“禁用Visual Studio托管进程”解决了这个问题。


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








嗨, 我需要在一个Visual C#项目中将两个大整型数组加载到内存中,每个数组都有约120万个元素(每个约470MB)。

当我尝试实例化第二个数组时,我会遇到OutOfMemoryException异常。

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

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

总结:

在Visual C#中使用两个大整型数组时发生了OutOfMemoryException异常,但是运行编译后的exe文件有效(内存使用量为1GB),而两个独立的Visual C#实例能够找到足够大的连续内存块来存储我的大数组,但我需要一个Visual C#实例来提供内存。


更新:

首先,特别感谢nobugz和Brian Rasmussen,我认为他们的预测“进程的2GB虚拟地址空间的分段”是问题的原因。

按照他们的建议,我使用VMMap和listdlls进行了简短的业余分析,并得到以下结果:
* “独立运行”的exe列出了21个dll(可以工作且使用1GB内存)
* vshost.exe版本列出了58个dll(在调试时运行,抛出异常且只使用500MB内存)

VMMap向我展示了调试器版本中最大的可用内存块为262,175,167,155,108MB。
因此,VMMap表示没有连续的500MB块,并根据有关可用块的信息,我添加了大约9个较小的int数组,其总计超过1.2GB内存使用量,实际上可以正常工作。
因此,从这个角度来看,“2GB虚拟地址空间的分段”是罪魁祸首。

通过listdll输出,我创建了一个小型电子表格,将十六进制数字转换为十进制以检查dll之间的空闲区域,并在(21)个dll之间找到了独立版本的大型空闲空间,但未在vshost-debugger版(58个dll)中找到。我并不认为我在那里做的事情一定正确,也不确定是否还有其他因素,但这似乎符合VMMap的分析,并且似乎dll本身就已经使调试器版本的内存发生了分段。

因此,也许解决方案是减少调试器使用的dll数量。
1.这可行吗? 2.如果可以,我该如何做?


1
你似乎认为这与Visual Studio有关。Visual Studio是你的应用程序的编辑器和编译器,但它之后独立运行。这与Visual Studio无关。 - John Saunders
是的,我认为这与Visual C#有关。你说“它自己运行了”,但是当我真正让它自己运行,换句话说,当我运行编译后的可执行文件时,我实际上没有任何问题。问题只会在使用Visual C#调试器运行时不断出现。 - user282727
@cusack:你在问题中没有提到调试器!!!请更新你的问题并加入这个信息。 - John Saunders
@John Saunders: 既然您没有花时间阅读我的问题,请不要浪费时间编辑它,谢谢。
  1. 您进入这个线程只给了我一个错误结论的两行评论,并基于这个错误结论恶化了我的线程标题。 我已经在第一个版本中声明可执行文件本身是正常的,问题只出现在Visual C#中。 根据您的评论,我立即更新了我的问题,补充说明我指的是调试器。
- user282727
  1. 在我已经更新了我的问题之后,你又要求我更新它。所以,你再次没有花时间阅读我的问题。
  2. 你再次编辑我的问题,删除了关于这是Express版本的信息,这是我第一次听说额外的信息是不好的。
既然你所做的一切都没有帮助,我恳请你礼貌地离开这个问题,提前感谢你。感谢所有花时间阅读和帮助的人。干杯!
- user282727
显示剩余6条评论
7个回答

9
您正在与虚拟内存地址空间碎片化作斗争。在32位Windows上,一个进程有2GB的可用内存。该内存由代码和数据共享。代码块包括CLR、JIT编译器以及nged框架程序集。数据块是.NET使用的各种堆,包括加载器堆(静态变量)和垃圾回收堆。这些块位于内存映射中的各个地址。可用的自由内存供您分配数组。

问题在于,大型数组需要连续的内存块。地址空间中“空洞”(代码和数据块之间的空隙)不足以允许您分配如此大的数组。第一个空洞通常在450到550兆字节之间,这就是为什么您的第一个数组分配成功的原因。下一个可用的空洞要小得多。太小了,无法容纳另一个大数组,即使您还有轻松的1GB可用内存,也会出现OOM。

您可以使用SysInternals的VMMap工具查看进程的虚拟内存布局。用于诊断问题,但不会解决您的问题。唯一真正的解决方法是迁移到64位版本的Windows。也许更好的解决方案是重新考虑您的算法,使其不需要如此大的数组。

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

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

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


你可以在新应用程序中加载第二个数组的原因是每个进程都有完整的2 GB虚拟地址空间。也就是说,操作系统会交换页面,以允许每个进程寻址总内存量。当您尝试在一个进程中分配两个数组时,运行时必须能够分配所需大小的两个连续块。你在数组中存储了什么?如果存储对象,则需要为每个对象提供额外的空间。
请记住,应用程序实际上并不请求物理内存。相反,每个应用程序都被赋予一个地址空间,从中它们可以分配虚拟内存。然后,操作系统将虚拟内存映射到物理内存。这是一个相当复杂的过程(Russinovich在他的《Windows Internal》一书中花费了100多页来介绍Windows如何处理内存)。有关Windows如何执行此操作的更多详细信息,请参见http://blogs.technet.com/markrussinovich/archive/2008/11/17/3155406.aspx更新:我已经思考了一段时间这个问题,它听起来有点奇怪。当您通过Visual Studio运行应用程序时,根据您的配置,您可能会看到加载了其他模块。在我的设置中,由于分析器和TypeMock(本质上是通过分析器钩子完成其魔术)的原因,我在调试期间加载了许多不同的DLL。

根据它们的大小和装载地址,它们可能会阻止运行时分配连续的内存。话虽如此,仅分配两个这些大型数组后就出现OOM,其组合大小不到1 GB,仍然让我有点惊讶。

您可以使用SysInternals的listdlls工具查看加载的DLL。它将显示加载地址和大小。或者,您可以使用WinDbg。lm命令显示已加载的模块。如果您还想要大小,请指定详细输出的v选项。WinDbg还允许您检查.NET堆,这可能有助于确定为什么无法分配内存。

第二次更新:如果您使用的是Windows XP,您可以尝试重新定位一些已加载的DLL文件,以释放更多的连续空间。Vista和Windows 7使用ASLR,因此我不确定在这些平台上重新定位是否会有所裨益。

1
只是补充一下,在32位Windows操作系统上,一个进程只能寻址最多2GB的内存,即使安装了更多的内存。 - Oded
这只是一个 int 数组,没有附加数据,基本上是一个大的查找表。请注意,可执行文件本身可以使用 ~1GB 运行而不会出现问题,但在 Visual C# 中进行调试时会导致异常,尽管它甚至没有使用接近 2GB 的任何东西,只使用了第一个数组的 0.5GB,并为第二个数组提供了异常。因此,我的问题基本上是如何使 Visual C# 更“贪婪”。感谢博客链接。 - user282727
ASLR不仅仅是针对系统dll的吗?如果他正在加载许多自定义dll,它们不应该能够被重新定位吗? - Lasse V. Karlsen
@Lasse:可能是这样,我真的不清楚细节。我只是想说在Vista及以后的系统上可能没有任何区别。 - Brian Rasmussen
好的,我希望你知道 :) 我们也遇到了dll重叠的问题,虽然还没有出现内存不足的问题,但我希望成功地重新定位所有dll(或至少是其中的许多)将使终端服务器能够为许多客户端重复使用一个物理内存块,而不是为每个客户端重新定位它,这似乎是它所做的。 - Lasse V. Karlsen

3
这不是一个具体的答案,但也许有个替代方案可行。如果问题确实是内存碎片化,那么也许一个解决方法是利用那些空洞,而不是试图找到一个足够大的连续空间。
这里有一个非常简单的BigArray类,它不会增加太多开销(引入了一些开销,特别是在构造函数中为了初始化桶)。该数组的统计信息如下:
- Main执行时间为404ms - 静态Program-constructor没有显示
该类的统计信息如下:
- Main执行时间为473ms - 静态Program-constructor需要837ms(初始化桶)
该类分配了一堆8192元素的数组(13位索引),在64位引用类型上,这将低于LOB限制。如果您只打算使用Int32,您可能可以将其提高到14,甚至可能使其成为非泛型,尽管我怀疑这样做不会提高性能。
在另一个方向上,如果您担心有很多小于8192元素的空洞(64位上为64KB或32位上为32KB),您可以通过其常量减少桶索引的位数。这将增加构造函数的开销,并增加更多的内存开销,因为最外层的数组将变得更大,但性能不应受影响。
以下是代码:
using System;
using NUnit.Framework;

namespace ConsoleApplication5
{
    class Program
    {
        // static int[] a = new int[100 * 1024 * 1024];
        static BigArray<int> a = new BigArray<int>(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<ArgumentOutOfRangeException>(() =>
            {
                new BigArray<int>(0);
            });
        }

        [Test]
        public void Constructor_NegativeLength_ThrowsArgumentOutOfRangeException()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                new BigArray<int>(-1);
            });
        }

        [Test]
        public void Indexer_SetsAndRetrievesCorrectValues()
        {
            BigArray<int> array = new BigArray<int>(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<int> array = new BigArray<int>(PRIME_ARRAY_SIZE);
            Assert.Throws<IndexOutOfRangeException>(() =>
            {
                array[PRIME_ARRAY_SIZE] = 0;
            });
        }

        [Test]
        public void Indexer_RetrieveElementJustBeforeStart_ThrowsIndexOutOfRangeException()
        {
            BigArray<int> array = new BigArray<int>(PRIME_ARRAY_SIZE);
            Assert.Throws<IndexOutOfRangeException>(() =>
            {
                array[-1] = 0;
            });
        }

        [Test]
        public void Constructor_BoundarySizes_ProducesCorrectlySizedArrays()
        {
            for (int index = 1; index < 16384; index++)
            {
                BigArray<int> arr = new BigArray<int>(index);
                Assert.That(arr.Length, Is.EqualTo(index));

                arr[index - 1] = 42;
                Assert.That(arr[index - 1], Is.EqualTo(42));
                Assert.Throws<IndexOutOfRangeException>(() =>
                {
                    arr[index] = 42;
                });
            }
        }
    }

    public class BigArray<T>
    {
        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;
            }
        }
    }
}

1

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


谢谢您的建议,但是由于我的int数组是一个大型查找表(全部关于速度),所以不幸的是列表并不适合。 - user282727
此外,.NET 中的 List<T> 类在内部使用数组,因此除非您指的是不同类型的列表(如链表),否则不会有太大帮助。 - Lasse V. Karlsen

0

我有两个桌面应用程序和一个移动应用程序超出内存限制的经验。我了解这些问题。我不知道您的要求,但我建议将您的查找数组移入SQL CE。性能很好,您会感到惊讶,而且SQL CE是进程内的。在最后一个桌面应用程序中,我能够将我的内存占用从2.1GB减少到720MB,这有助于通过显著减少页面故障来加速应用程序。(您的问题是AppDomain内存的碎片化,您无法控制。)

老实说,我认为您在将这些数组挤入内存后不会满意性能。别忘了,过多的页面故障对性能有重大影响。

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

如果您真的想知道内存发生了什么,请使用CLR Profiler。VMMap没有帮助。操作系统不会为您的应用程序分配内存。框架通过获取大块操作系统内存(缓存内存),然后在需要时将这些内存的一部分分配给应用程序。

针对.NET Framework 2.0的CLR分析器,位于https://github.com/MicrosoftArchive/clrprofiler


0
一个问题:您的数组中所有元素都被占用了吗?如果其中许多元素包含默认值,那么也许您可以使用稀疏数组的实现来减少内存消耗,因为它只为非默认值分配内存。这只是一个想法。

0
每个32位进程具有2GB的地址空间(除非您要求用户在启动选项中添加/3GB),因此,如果您可以接受一些性能降低,您可以启动一个新进程以获得2GB以上的地址空间 - 嗯,略少于此数。新进程仍将与所有CLR dll及其使用的所有Win32 DLL片段化,因此,您可以通过使用本机语言(例如C ++)编写新进程来摆脱CLR dll引起的所有地址空间片段化。您甚至可以将一些计算移到新进程中,以便在主应用程序中获得更多的地址空间并减少与主进程的通信次数。
您可以使用任何进程间通信方法在进程之间通信。您可以在All-In-One Code Framework中找到许多IPC示例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接