看了firelong写的C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题, Ivony写的C#呓语:谁说程序都要加载到内存?和后面的很多评论后,觉得需要写点来表达一些观点。希望能同大家一起探讨。
firelong在C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题中提及:
" 程序(EXE/DLL)最后都是要加载到内存中运行的,不是光放在硬盘上的——这也是为什么.NET程序占用内存都超多"
有几位园友在评论中指出这句是错误的或者质疑firelong的结论。如:
Jeffray Zhao: "不用,不JIT的话,是不会把整个dll加载到内存中的,而是用多少加载多少,这点已经讨论了很多遍了"
道法自然:".NET类加载器,在一个方法一个方法循环调用时,仅是在一个方法调用前,才会加载这个方法使用的类型。这一个是类型加载的时机。另一个问题,类型加载时,包含了什么内容?也就是说,加载类型时,都加载了什么样的元数据?"
1. 关于.net程序集加载
虽然在.net方面工作过几年,但是说实话在此文之前我并没有对.net程序集加载有一个清晰的概念。这些个园友都是些有经验的人,为什么他们说的观点相反。到底.net程序集加载是整个地加载?还是如Jeffray Zhao等所说"用多少加载多少", "用到那个类型就加载该类型"。刚好最近在看<CLR via c#> 第三版英文版,试图在书中找到答案。但是有点失望的是,书中并没有很详细地说。只是有一段提及了.net程序集加载,然后就不再深入说了。Jefferay Zhao等几位所持的观点没有办法在书中得到验证。只好自己来验证。刚好Ivony写了C#呓语:谁说程序都要加载到内存?,我就接着他的例子去做一下吧。Ivony的文章结论是没有用到的metadata是不会加载到内存。我可以重复一下Ivony的实验,看看我是否也得出同样的结论(有点象物理化学的实验, 是不是?)。
我首先照Ivony说的过程重复了一下他做过的步骤,产生了一个巨大的Test.cs文件,里面包含100个类,100个方法,100个属性,发现确实如他所说:在任务管理器里面看那个ConsoleApplication1.exe占用只有1MB 多一点的内存空间。而编译出来的ConsoleApplication1.exe有10MB。初步看起来,Console1Application1.exe没有全部加载进内存,也即100个类的metadata没有加载进内存。
此后两天我都在想为什么呢?真如他们几位所说,那么CLR具体是如何在内存里面组织这些metadata,类型,是如何JIT编译方法的。想不透,只好祭出强大的工具: Windbg来找找答案。这回有一些不同的发现:
0:000> lmu
start end module name
00040000 00ac4000 ConsoleApplication1 (deferred)
634b0000 63fa8000 mscorlib_ni (deferred)
64ab0000 65040000 mscorwks (deferred)
6b040000 6b0a6000 mscoreei (deferred)
6e2a0000 6e2fb000 mscorjit (deferred)
6e570000 6e5ba000 mscoree (deferred)
73950000 739eb000 MSVCR80 (deferred)
74db0000 74f4e000 comctl32 (deferred)
76640000 766ea000 msvcrt (deferred)
766f0000 7673b000 GDI32 (deferred)
76740000 7681c000 KERNEL32 (deferred)
76820000 76879000 SHLWAPI (deferred)
76880000 76946000 ADVAPI32 (deferred)
769a0000 76a3d000 USER32 (deferred)
76a40000 76b03000 RPCRT4 (deferred)
76b10000 77620000 shell32 (deferred)
777b0000 77878000 MSCTF (deferred)
77880000 779c5000 ole32 (deferred)
779d0000 77a4d000 USP10 (deferred)
77b40000 77c67000 ntdll (export symbols) C:\Windows\system32\ntdll.dll
77c90000 77c99000 LPK (deferred)
77cb0000 77cce000 IMM32 (deferred)
0:000> !DumpDomain
--------------------------------------
System Domain: 64ffd058
LowFrequencyHeap: 64ffd07c
HighFrequencyHeap: 64ffd0c8
StubHeap: 64ffd114
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 64ffc9a8
LowFrequencyHeap: 64ffc9cc
HighFrequencyHeap: 64ffca18
StubHeap: 64ffca64
Stage: OPEN
Name: None
Assembly: 00d867a8
--------------------------------------
Domain 1: 00d41bd8
LowFrequencyHeap: 00d41bfc
HighFrequencyHeap: 00d41c48
StubHeap: 00d41c94
Stage: OPEN
SecurityDescriptor: 00d42f00
Name: ConsoleApplication1.exe
Assembly: 00d867a8 [C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 00d86828
SecurityDescriptor: 00d7a7b8
Module Name
634b1000 C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
Assembly: 00d8fc60 [C:\Users\Administrator\Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe]
ClassLoader: 00d91680
SecurityDescriptor: 00d8fb60
Module Name
00d12c5c C:\Users\Administrator\Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe
0:000> !Dumpmodule 00d12c5c
No export Dumpmodule found
0:000> !DumpModule 00d12c5c
Name: C:\Users\Administrator\Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Release\ConsoleApplication1.exe
Attributes: PEFile
Assembly: 00d8fc60
LoaderHeap: 00000000
TypeDefToMethodTableMap: 022f0010
TypeRefToMethodTableMap: 022f07ec
MethodDefToDescMap: 022f083c
FieldDefToDescMap: 02352a98
MemberRefToDescMap: 02352aa4
FileReferencesMap: 02352af8
AssemblyReferencesMap: 02352afc
MetaData start address: 00073d94 (10797316 bytes)
0:000> db 00073d94 l 1000
00073d94 42 53 4a 42 01 00 01 00-00 00 00 00 0c 00 00 00 BSJB............
00073da4 76 32 2e 30 2e 35 30 37-32 37 00 00 00 00 05 00 v2.0.50727......
00073db4 6c 00 00 00 30 91 2c 00-23 7e 00 00 9c 91 2c 00 l...0.,.#~....,.
00073dc4 54 2e 78 00 23 53 74 72-69 6e 67 73 00 00 00 00 T.x.#Strings....
00073dd4 f0 bf a4 00 1c 00 00 00-23 55 53 00 0c c0 a4 00 ........#US.....
00073de4 10 00 00 00 23 47 55 49-44 00 00 00 1c c0 a4 00 ....#GUID.......
00073df4 e8 00 00 00 23 42 6c 6f-62 00 00 00 00 00 00 00 ....#Blob.......
00073e04 02 00 01 10 57 15 a2 01-09 00 00 00 00 fa 25 33 ....W.........%3
00073e14 00 16 00 00 01 00 00 00-13 00 00 00 f6 01 00 00 ................
00073e24 02 00 00 00 96 88 01 00-51 c3 00 00 13 00 00 00 ........Q.......
00073e34 0d 00 00 00 01 00 00 00-f4 01 00 00 50 c3 00 00 ............P...
00073e44 50 c3 00 00 01 00 00 00-01 00 00 00 00 00 0a 00 P...............
......省略很多
注意看我用黄色背景加亮的那些数字。前两个数字是这个ConsoleApplication1.exe所在的地址。注意其长度。后两个是metadata的起始地址及长度。注意其长度是大约10MB。
以上的Windbg记录显示我们的程序集虽然有10MB之巨,但是.net CLR还是将该程序集全部载入内存。metadata也随该程序集一起被CLR载入内存。不管是用到了类型的metadata,还是没有用到类型的metadata,都被载入了内存。这样就很清晰了。至少firelong这半句话是对的: "程序(EXE/DLL)最后都是要加载到内存中运行的,不是光放在硬盘上的", 那么为什么任务管理器显示ConsoleApplication1.exe只占用1MB左右的内存呢?这个问题有好几天我都没有办法解释。只到今天,我突然想起来用其他工具来查看进程的内存占用。结果令我恍然大悟: 原来任务管理器统计的内存占用不准确。那么好了,结论就是程序集是整个地被加载进内存的,不是"用多少加载多少", "用到那个类型就加载该类型".
2. 关于firelong所说的这后半句话:"这也是为什么.NET程序占用内存都超多"
firelong认为: 我们自己编译出来的程序集, metadata占太大比例的空间, 有50%以上。.net FCL本身的程序集metadata也占比较大的空间. metadata对性能影响很大。 我给解释一下:
我们自己编译出来的程序集, metadata的大小取决于设计,开发者设计了很多类型,那metadata自然小不了。你想想有没有过度设计,减少点设计复杂度,类型少一点,metadata的尺寸自然会小点。
.net FCL本身的程序集, 我们可以先说说mscorlib.dll, 它是每一个.net应用程序必引用的. 它大约就5M左右,它是运行每一个.net应用程序必须的负载(overhead), 你的机器不会连5M内存消耗都承受不起吧。 至于其他的.net FCL的程序集, 是用到了才会加载,不是必须的。.net FCL设计已经相当精炼了。另外还有一个.net CLR提供的特性可以帮助减少.net 应用程序的内存消耗:domain neutral, 有domain neutral特性的程序集都是跨AppDomain共享的,那么在一个进程的内存里面只要一份mscorlib.dll的拷贝就行了。.net FCL的程序集都是domain neutal的。还有asp.net下我们的应用程序集也是domain neutral的. 通过这些手段, 微软.net团队已经大大减少了.net应用程序的内存负荷。
另外,大家可以注意一下你的应用程序里面的线程,有时候线程多也是造成内存占用大的原因。
总之firelong认为metadata过大影响性能是没有站不住脚的,实际上那只与你的设计有关。firelong将.net平台, c#编程语言与c/c++相比,并不合适. 这些编程平台各自有各自的特点,结合自己真实的需要才是正确的。