32 位与 64 位内存






4.64/5 (9投票s)
您的应用程序内存为何依赖于平台?
目标
本文旨在帮助读者理解为什么同一段代码在 32 位环境、WOW(Windows on Windows)环境和 64 位环境中运行时会消耗不同的内存。
引言
众所周知,在 WOW 上运行应用程序比在 32 位环境中消耗更多内存,而在 64 位环境中运行则比在 WOW 和 32 位环境中消耗更多内存。
虽然没有一个确切的公式可以计算出内存增加的确切百分比,但下面的讨论通过比较 32 位与 64 位以及 32 位与 WOW,有助于理解导致内存使用量增加的原因。
背景
对 WOW、32 位应用程序、64 位应用程序以及平台移植有基本了解的读者将能从本文中获益最多。
为了更加安全起见,如果您决定无论如何都要阅读本文,让我们用一句话来总结 WOW。
"WOW 模拟了一个与底层平台不同的环境,以便原本不兼容的应用程序现在可以运行。"
32 位与 64 位
64 位和 32 位之间最主要的区别是地址字段的宽度,从 4 字节增加到 8 字节。因此,显而易见,我们的应用程序中地址字段/指针的数量越多,64 位环境下的内存消耗就越多。本文旨在通过遍历 .Net 进程来探索进程内部不同位置的所有底层地址字段/指针,这些最终会负责 64 位进程相对于 32 位进程的内存消耗增加。
数据对齐
内存增加的主要原因之一是数据对齐。数据对齐是将数据放置在内存偏移量上,该偏移量是“字”(WORD)大小的倍数。
当处理器从内存读取或写入内存时,是以“字”大小的块进行的,在 32 位环境中是 4 字节,在 64 位环境中是 8 字节。当给定对象的大小不是“字”大小的倍数时,操作系统将不得不将其填充到下一个“字”大小的倍数。这是通过在对象末尾添加一些无意义的信息(填充)来实现的。
在 32 位 .Net 进程中,对象的大小至少为 12 字节,而在 64 位 .Net 进程中,则至少为 24 字节。包含两个指针类型的头部在 32 位和 64 位环境中分别占用 8 字节和 16 字节。即使对象没有任何成员,它也会额外消耗 4 字节/8 字节用于填充,这使其在 32 位和 64 位进程中分别达到 12 字节和 24 字节。
如果一个 32 位环境中的对象只有一个 short 成员,该对象的大小理想情况下应该是头部大小(8 字节)+ short 大小(2 字节)。但是,最终的大小是头部大小(8 字节)+ short 大小(2 字节)+ 填充字节(2 字节),因此数据对齐会导致不可避免的内存浪费。这种浪费在 64 位环境中会加剧,仅仅因为“字”大小变为 8 字节而不是 4 字节。如果 64 位环境中的一个对象只有一个 short 成员,它将需要 24 字节(16 字节的头部 + 2 字节的 short + 6 字节的填充/调整)。浪费/填充不是每个对象的固定因素,而是取决于该对象的“类型”和“数量”的成员。
例如:包含一个 `int` 并占用 24 字节的对象,以相同的成本可以容纳 2 个 `int`,从而实现零浪费。一个占用 24 字节且仅包含一个 `short` 的对象,以相同的成本可以容纳 4 个 `short`,从而实现零浪费。
对象头
任何 .NET 对象都会有一个同步块(指向同步块的指针)和一个类型句柄(方法表指针)。在 64 位环境中,头部大小增加了 8 字节,因为头部本质上包含两个指针,而在 64 位环境中指针是 8 字节,而在 32 位环境中是 4 字节。这意味着,如果一个应用程序有 10000 个对象(无论什么类型),在 32 位和 64 位环境之间,内存将直接增加 80000 字节,即使它们是空对象。
技术栈
进程的堆栈段也会导致 64 位内存的增加。堆栈中的每一行/项都有两个指针,一个用于调用者地址,另一个用于返回地址。为了感受其贡献的重要性,让我们考虑下面的程序。
namespace Memory_Analysis { class Program { static void Main(string[] args) { A obj = new A(); Console.ReadLine(); } } class A { char Data1; char Data2; short Data3; int Data4; }
该代码执行时的堆栈段大约有 6000 行(通过 SOS 测量)。6000 行将导致 1200 个地址(指针)字段,因为如上所述,堆栈中的每一行都有两个地址:调用者地址和返回地址。每个地址字段在 64 位环境中会导致增加 4 字节。因此,对于像上面这样小的代码段,堆栈段本身将增加(1200 * 4)4800 字节。
方法表
现在来看方法表,每个至少有一个活动实例的类都有一个方法表。每个方法表又包含 2 个地址字段(入口点和描述)。如果一个应用程序包含 100 个方法,包括其中的所有类,那么仅仅因为方法表,在 64 位环境中就会导致(100 * 2)* 4 = 800 字节的内存增加。类似地,其他具有地址字段并导致内存增加的还有 GCHandles 和 FinalizationQueue。
Assemblies
除了堆栈和堆之外,加载到其 AppDomain 的程序集也导致内存增加。下面是 AppDomain 头部的快照。我们可以看到,头部至少有 15 个地址字段。
Parent Domain: 0014f000 ClassLoader: 001ca060 System Domain: 000007fefa1c5ef0 LowFrequencyHeap: 000007fefa1c5f38 HighFrequencyHeap: 000007fefa1c5fc8 StubHeap: 000007fefa1c6058 Stage: OPEN Name: None Shared Domain: 000007fefa1c6860 LowFrequencyHeap: 000007fefa1c68a8 HighFrequencyHeap: 000007fefa1c6938 StubHeap: 000007fefa1c69c8 Stage: OPEN Name: None Assembly: 00000000011729a0 Domain 1: 00000000003a34a0 LowFrequencyHeap: 00000000003a34e8 HighFrequencyHeap: 00000000003a3578 StubHeap: 00000000003a3608 Stage: OPEN SecurityDescriptor: 00000000003a4d40 Name: ConsoleApplication2.vshost.exe
在头部之后,Appdomain 将包含 AppDomain 内所有程序集的列表。在每个程序集下,它又包含该程序集中所有模块的列表。
下面是程序集及其内部模块列表的快照的一部分。下面的快照仅包含 AppDomain 中引用我们示例“Memory_Analysis”程序集的那个部分。
Assembly: 000000001ab3c330 [C:\Users\ing06996\Documents\Visual Studio 2008\Projects\ Memory_Analysis \ Memory_Analysis \bin\x64\Debug\ Memory_Analysis.exe] ClassLoader: 000000001ab3c3f0 SecurityDescriptor: 000000001ab3b5b0 Module Name 000007ff001d1b08 C:\Users\ing06996\Documents\Visual Studio 2008\Projects\ Memory_Analysis \ Memory_Analysis \bin\x64\Debug\ Memory_Analysis.exe
加载我们示例应用程序“Memory_Analysis”的 AppDomain 还必须加载我们示例应用程序的所有引用 DLL,包括 MSCOREE.dll 和 MSCORWKS.DLL 等 .Net DLL。对于每个这样的引用 DLL,都会有与上面快照类似的条目。
此外,在每个模块内部,会有几个地址字段,如下面的快照所示。
Assembly: 0090ae40 LoaderHeap: 00000000 TypeDefToMethodTableMap: 00170148 TypeRefToMethodTableMap: 00170158 MethodDefToDescMap: 001701a4 FieldDefToDescMap: 001701b4 MemberRefToDescMap: 001701c8 FileReferencesMap: 00170214 AssemblyReferencesMap: 00170218 MetaData start address: 0016207c
通过 SOS 和 WinDBG 测量,像我们的“Memory_Analysis”这样的简单程序集在 AppDomain 中大约有 80 个地址字段,这意味着内存增加(80 * 4)320 字节。引用程序集越多,模块越多,内存消耗就越高。
32 位与 WOW
在比较了 32 位与 64 位之后,现在让我们探讨在 32 位环境中运行 32 位进程与在 WOW 环境中运行 32 位进程之间的区别。
如我们所知,WOW(Windows on Windows)是一个模拟环境,其中 64 位操作系统提供 32 位环境,以便 32 位进程可以无缝运行。讨论的触发点是,在 WOW 上运行 32 位进程比在 32 位环境中运行 32 位进程消耗更多内存。下面的讨论试图探讨在 WOW 上运行导致内存消耗增加的一些原因。在找出内存增加的原因之前,了解增加的幅度很重要。同样,没有公式可以计算出在 WOW 模式下运行时内存增加的确切百分比。尽管如此,一个示例和一些解释可能有助于我们了解增加的幅度。让我们考虑下面的代码。
class MemOnWOW { int i; private MemOnWOW() { i = 10; } static void Main(string[] args) { MemOnWOW p = new MemOnWOW(); System.Console.ReadLine(); } }
当为 32 位平台构建并在 32 位环境中执行时,它消耗的总大小为 80,596 KB,而在 WOW 环境中执行时,它消耗的总大小为 115,128 KB,这意味着增加了 34,532 KB。** 总大小:总大小包括托管堆、非托管堆、堆栈、映像、映射文件、可共享、私有数据和分页表。现在,让我们列出总内存中每个段对内存增加的贡献。
WOW 映像和执行引擎
位于 C:\windows\sysWOW64 位置的 WOW 的程序集,增加了约 12MB 的内存。这些程序集是启动和执行 WOW 环境所必需的,32 位进程在该环境中运行。下面是一些启动 WOW 环境所需的 DLL 及其作用的列表。
- Wow64.dll
管理进程和线程创建。异常分派文件系统重定向注册表重定向
- Wow64Cpu.dll
管理 WOW64 中每个正在运行线程的 32 位 CPU 上下文。提供特定于处理器架构的支持,用于在 32 位和 64 位 CPU 模式之间切换。
- Wow64Win.dll
拦截 GUI 系统调用。
理解 WOW 执行环境增加的开销非常重要。您可能想仔细阅读 Mark Russinovich 在其《Windows Internals》一书中给出的详细解释。
每当有系统调用被发送到底层内核(调用内核公开的 API)或从内核传来回调时,输入参数和输出参数都必须从 32 位转换为 64 位,从 64 位转换为 32 位。
位于中间的 WOW 负责在将 32 位用户模式对象发送到内核之前将其转换为 64 位用户模式对象。类似地,在内核抛出异常对象并需要将其传递给用户应用程序之前,WOW 也必须将 64 位内核对象转换为 32 位内核对象。当内核抛出异常对象并需要将其传递给用户应用程序时,也会发生类似的转换。这也称为“异常挂钩/异常分派”。显而易见,在上述创建其他中间(用户和内核)对象的场景中,内存必然会增加。这些内核模式和用户模式之间的转换也可能导致性能下降,而不仅仅是内存增加。
映射文件
在 .Net 应用程序执行期间,可能会有几个文件被内存映射。其中一些是全球化文件(*.nls)、字体文件(*.ttf)等。在 WOW 下运行时,还有额外的 WOW 相关文件被内存映射,从而导致内存增加。被内存映射的文件数量取决于引用的程序集类型、应用程序内使用的资源等。对于我们考虑的“MemOnWOW”示例,WOW 模式下的其他映射文件是与全球化相关的 .nls 文件,它们消耗约 2.5 MB 的额外内存。
非托管堆
在 WOW 执行下,非托管堆将被 WOW 环境使用。在我们目前的示例中,它消耗了进程内存的约 2 MB。当 32 位托管应用程序直接在 32 位环境中运行时,将永远不会使用此非托管堆。
托管堆
托管堆会保持中立,无论是在 WOW 还是直接的 32 位环境中,消耗的内存量都完全相同。
私有数据
当进程在 WOW 下运行时,由于处于 WOW 模式,会增加约 18 MB 的私有数据到进程的总内存大小。如果由于应用程序本身而有其他私有数据,它将在此 18MB(在 WOW 中)之上。
堆栈段
当在 WOW 上运行时,堆栈对内存增加的贡献因应用程序而异,因为它取决于程序长度(堆栈有多长)。我们知道,堆栈用于存储每个线程的函数参数、局部函数变量和函数调用记录(谁调用了该函数)。如果有 3 个线程,则会有 3 个不同的堆栈在堆栈段中。
在 WOW 下运行时,对于每个线程,堆栈段都必须维护 2 个不同的独立堆栈:一个作为 32 位堆栈,另一个作为 64 位堆栈。因此,如果一个应用程序中有 3 个线程,在 WOW 上运行时,堆栈段中将有 6 个不同的堆栈。
参考文献
- VMMAP 文档
- Mark Russinovich 的 Windows Internals
- blogs.Technet.com