在“AnyCPU”平台目标上的内存分配模式

6
我有意在一个简单的C#程序中故意泄漏内存,以更好地了解.NET如何管理这个方面。这是通过声明每100ms一个大小为1000万的int[]数组来完成的。为了不将数据带入进程的工作集中,数组的元素没有被“触摸”,即未赋值。
const int BlockSIZE = 10000000;  // 10 million
const int noOfBlocks = 500;
int[][] intArray = new int[noOfBlocks][];

for (int k = 0; k < noOfBlocks; k++) {
    intArray[k] = new int[BlockSIZE];
    Console.WriteLine("Allocated (but not touched) for array {0}: {1} bytes", k, BlockSIZE);
    System.Threading.Thread.Sleep(100);
}

我正在使用由Mark Russinovich开发的VMMap工具来查看内存分配情况。版本是最新的(2018年发布的3.25版本),因此它了解托管堆。
在x64 Windows 10机器上使用Visual Studio 2015编译并生成.exe文件,RAM为8 GB。根据项目构建部分中的平台目标设置,可以看到与内存分配方式相关的不同结果,如下所示。
当平台目标设置为x86时,已提交的内存增长直到接近2 GB,然后抛出内存不足错误。这个值是可以预期的,因为2 GB是x86体系结构用户虚拟地址空间的限制(我没有使用IncreaseUserVA,否则会将其提升到3 GB。稍后编辑:这不完全正确-请参见David的答案)。在这种情况下,VMMap的输出如下所示。大部分提交的数据都属于托管堆类别,这是可以预料的。

Platform target= x64

平台目标 设置为 x64 时,承诺的区域会像预期的那样不断增长。最终应用程序需要被杀死,因为它不断地分配内存。这也是预期的,因为只要可用的 RAM + 分页文件的总数量可以容纳增长,64位Win10框的理论限制是每个用户虚拟地址空间128 TB(由于当前处理器仅使用64位中的48位作为虚拟地址,因此受限)。VMMap的输出如下所示。同样,大部分已承诺的字节属于托管堆类别。

enter image description here

平台目标 设置为 Any CPU 且勾选了 Prefer 32-bit - 这实际上是在 Visual Studio 2015 中的默认设置 - 结果并不那么简单。首先,当已提交内存达到约3.5 GB时,会抛出内存不足异常。其次,在托管堆中,私有字节仅增长到约1.2 GB,之后 Private Data 类别会注册下一个分配的数据。VMMap 的输出如下。

enter image description here

为什么在“Any CPU”+“Prefer 32-bit”设置下,分配会如最后一段所述?具体来说,为什么相当大量的数据列在私有数据而不是托管堆中?
后续编辑:内联添加图片以获得更好的清晰度。

3
这是VMMap中的小错误。它只将2GB以下的内存视为托管堆。这在以前的旧时代是正常的,因为编译器开始将exe文件标记为/largeaddressaware之前就是这样工作的。不,1.8GB私有数据也是托管堆。请注意测试结果并不具有代表性,您需要访问数组以实际占用内存。这就是为什么工作集现在如此之低的原因。对于int []数组,您需要读取每1024个元素来使其占用内存。 - Hans Passant
基于托管堆在针对x64时表现良好的事实,我期望它的行为是一致的。然而,当启动VMMap时,有一个32位应用程序和一个64位应用程序同时运行。我猜测是32位应用程序正在分析我的/largeaddressaware .exe文件并得出了错误的结论。至于测试本身,我没有特别涉及页面,以使工作集很低 - 我的目标是理解第三个模式中在VMMap中只看到已提交内存的奇怪情况。 - Mihai Albert
关于你提到的每1024个元素读取一次的问题:这可能是因为int占用4个字节,而1024个int数组元素将完全填充一个常规的4KB页面(int数组将强制进行连续分配)。仅触摸第1024个元素将触发内存管理器将整个页面带入内存,作为工作集的一部分。只要周围有空闲内存,并且这些页面都没有移动到分页文件中,就可以将整个已提交的内存视为工作集的一部分。我的理解正确吗? - Mihai Albert
1个回答

4
在Windows64(wow64)上运行的LARGEADDRESSAWARE 32位进程具有4GB用户模式虚拟地址空间(VAS),因为内核内存是64位的,不需要映射到32位指针可寻址的4GB中。而且你不需要使用/3GB开关启动Windows来获得它。
在编译X86时,您可能希望在32位和64位平台上实现相同的行为,因此不设置LARGEADDRESSAWARE标志是合理的。此外,这可能是受向后兼容性的影响。在非常古老的时代,一些32位库(误)使用指针的高位比特,因此历史上限制32位程序为2GB是一种安全设置。
AnyCPU+Prefer 32位是一个较新的设置,默认情况下设置了LARGEADDRESSAWARE,以便更好地访问64位平台上的资源。

好的,所以我的进程遇到了4 GB的限制。目前为止很清楚。但是在1.2 GB标记之后显示的大量私有数据是否只是VMMap中的一个错误 - 正如Hans在他的评论中指出的那样? - Mihai Albert
据我所知,“private data”不是一种艺术术语,可能只是指未经计算的私有(非共享)已提交内存。也许是大对象堆。 - David Browne - Microsoft
我使用了Visual Studio工具中的dumpbin.exe,并使用/headers选项来仔细检查生成图像的类型。就像你所说的那样,将平台目标设置为x86将生成一个没有LARGEADDRESSAWARE的x86图像;将其设置为x64将生成一个带有LARGEADDRESSAWARE的x64图像;将其设置为“任何CPU”和“优先32位”将生成一个带有LARGEADDRESSAWARE的x86图像。 - Mihai Albert
关于私有数据,你说得对。根据VMMap自己的帮助文件:“_私有内存是由VirtualAlloc分配的内存,不会被堆管理器或.NET运行时子分配_”。Windows Internals第7版将其定义为“_这显示除堆栈和堆之外标记为私有的内存分配,例如内部数据结构_”。因此,我只能认为这是VMMap中的一个错误,就像Hans提到的一样。 - Mihai Albert

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