【二】反射背后需要的支撑机制:元数据的性能问题——不使用反射的性能问题
要谈这个问题,首先大家应该清楚C#/.NET中反射的功能是由metadata来支持的,即便你所有的代码中、你用的所有Application Framework的代码中都没有使用一点反射的API,C#编译器还是会在最后生成的EXE或者DLL中生成所有的metadata。(如果这个不清楚,请先读Jeffrey Richter的《CLR via C#》一书)。而 Metadata就是C#/.NET性能的罪魁祸首!要理解这一点,大家先来做两个简单的针对metadata的分析。
1. 用ILDASM工具将C:\Windows\Microsoft.NET\Framework\v4.0.30128 下面的MSCorlib.dll(.NET核心类库程序集,其他版本也可以,不必非要4.0)打开。点击:View->Statistics,看一下其中的元数据大小:
CLR header size : 72 ( 0.00%)
CLR meta-data size : 2083724 (40.09%)
CLR additional info : 931312 (17.92%)
CLR method headers : 136967 ( 2.64%)
Managed code : 1212346 (23.32%)
Data : 753152 (14.49%)
注意:这四个部分,其要么是metadata,要么是metadata的辅助信息,所以我在后面文章中都算作元数据部分:
整个MSCorlib.dll大小为4.95M。
Metadata总共占用大约3.01M,占总大小大约60.6%。
真正传统的Code+Data总共占用大约1.87M,占总大小约37.8%。
MSCorlib.dll总共大小4.95M,为了支持反射,需要添加的元数据竟然有3.01M,占到60%的大小!!!我想大家已经看出问题来了。有些朋友可能会说,这是特例吧?别的DLL呢?
2. 我们再来随便找一个DLL,比如WPF的DLL:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll,同样适用ILDASM打开,点击:View->Statistics看一下其中的元数据大小:
整个PresentationFramework.dll大小为5.03M。Metadata总共占用大约55.15%!
大家可以随便拿一个自己项目中.NET的DLL或者EXE来分析,看看Metadata的大小占用多少? 基本都在50%以上,甚至有的高达70%!
这意味着什么?即使你不用任何反射的代码,C#/.NET为了让它支持反射,还要给你最后生成的DLL/EXE强加50%以上的metadata(这是强制的,即便你不用反射,C#/.NET也没有提供任何编译选项将这些metadata去掉)。这就是.NET Framework Redistributable本身要40M左右的原因!
我想这个铁的事实是“老赵们”无论如何都不能否认的。但是“老赵们”的典型言论马上又来了:
(1)不就是程序有点大吗?现在大硬盘很便宜,运行起来还是很快的
(2)就是.NET Framwork有点大,客户安装起来不方便
(3)大只是空间效率,不影响程序的时间效率
这些调调显然都是没有真正搞过“性能优化”的“老赵们”的浅见。空间效率并非对时间效率没有影响,而是有致命影响。一个100M的应用程序,运行起来肯定要比一个40M的程序慢许多。理由如下:
(1)程序(EXE/DLL)最后都是要加载到内存中运行的,不是光放在硬盘上的——这也是为什么.NET程序占用内存都较多
(2)占用内存多的程序,运行起来必然慢。因为内存大的程序必然会出现较多的page fault(即换页错误),cache missing(即缓存失效)(简单来说,要尽可能在CPU缓存中操作working set,CPU缓存装不下,就要跑到主存里面找;主存装不下就要跑到虚拟内存-也就是硬盘里面找,那样软件运行的性能代价非常高). Page fault和cache missing已经成为现代软件性能的一大公害。很多程序慢下来,如果不是蹩脚的算法,Page fault和cache missing往往都是罪魁祸首!关于这方面的理论,很多牛人都专门讲过,国外也有比较牛叉的咨询公司专门做这方面的优化,大家如果想深度理解这方面,可以参照:
a. CACHE MEMORY:IMPLEMENTATION ANDDESIGN TECHNIQUES
http://www.faculty.iu-bremen.de/birk/lectures/PC101-2003/07cache/cache%20memory.htm
b. Improving Managed Code Performance-Working SetConsiderations
http://msdn.microsoft.com/en-us/library/ff647790.aspx#scalenetchapt05_topic33
c.以及微软的.NET性能经理Rico Mariani在这里的文章:
My mom doesn't care about space,http://blogs.msdn.com/b/ricom/archive/2004/03/15/89934.aspx
所以,总结下来就是:
(1)Metadata非常占用空间,一般占到整个EXE/DLL总大小的50%~70%
(2)高昂的空间成本会由于Page fault和cache missing等因素转嫁为高昂的时间成本
(3)即便在代码中不写一行反射调用代码,所有的metadata仍然会生成,我们仍然要为此付出高昂的空间代价和时间代价。
比如,我们公司开发的一个大型医疗软件,之前的版本使用C++开发,整个生成代码体积为40M左右,但是转移到.NET平台上(被微软的.NET平台战略忽悠过来)后发现代码体积为130M左右(功能差不多的前提下,第一版主要是移植,新增功能的代码量占不到5%),我们反反复复怎么优化都优化不到原来的40M左右,最后发现都是反射惹的祸!——我相信我在前文举出的很多世界著名、或者中国著名的软件最终没有选择.NET,都有过这样一个评测过程。
其他的例子大家可以自己找,比如就拿mspaint.exe 与paint.net(到这里下载:http://www.softpedia.com/progDownload/Paint-NET-Download-19322.html)比较比较,功能差不多相同。运行一下看看,它们各占多少内存:前者5.7M,后者占用17.7M!3倍多!
软件size大,没关系,你要大在地方,比如因为功能原因,code多一些导致size大我接受。但是你50%-70%的size都去装metadata了,而我又不怎么用metadata(反射),你还要这么大放在那里,极大地损害软件性能。
这还是一个小小paint玩具软件!你让QQ、photoshop,office等软件用C#/.NET开发试试?除非是“老赵们”自己开公司玩。
反射性能问题总结
好了,我相信问题已经分析清楚了,总结一下到目前为止,这篇文章的重点:
1. 反射的绑定和调用成本很高
—— C#反射绑定与调用过程中元数据字符串比对,参数校验,安全校验,大量临时对象,会让使用C#反射时的软件性能很差,尽量避免使用
2. 你不使用某些性能低的功能,不代表你依附的Application Framework不使用这些功能
—— 目前.NET平台中WPF/SL, WCF,WF, ASP.NET MVC等几大核心的框架都很重地使用了反射
3. 有些功能即便程序中不使用,为了支持这种机制,也要付出很高的代价
—— 哪怕所有的代码都是你写(不用Application Framework),而且不用一点反射的功能,C#编译器还是给你的软件中加了很多支持反射的metadata,占用很高昂的空间成本(大约是整个软件size的50%)
4. 只要有较大的空间成本,那么时间成本也一定很高
—— 反射背后的metadata占用的高昂的空间成本,由于内存加载、working set、cache missing 等各种问题,直接导致的时间成本很大,严重影响软件的运行性能。
上面的分析方法、依据、包括数据都是我和公司美国、德国同事,在开发C#/.NET产品时(大型医疗软件),遇到的非常实际的问题(客户接受不了C#/.NET写的软件速度),用符合工程的系统、全面的分析方法,研究各领域专家的分析意见(包括很多微软技术专家),对C#/.NET进行的性能研究(不是写个CodeTimer玩具比较比较两段代码就叫性能分析),我们尝试了很多优化策略——最后的结论就是绕不开C#/.NET底层设计带来的根深蒂固的性能问题!反射就是一个性能公害!
好,相信看到这里,绝大多数朋友已经深入理解了“反射所带来的严重的性能问题”。但是有很多朋友可能还会有疑问,咦?怎么有些人写C#性能也不错,而且写得头头是道,似乎很有道理啊。到底谁说的对啊?
这样的疑问很正常,这些论调就是我前文说的“只见树木,不见森林”。为了理清网友的疑问,我在下面的小节中针对这些“一叶障目”的观点进行一一戳穿,以便于大家今后明辨是非。