C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题
理清几个基本点在开始谈论性能问题之前有必要首先理清几个基本点。我们谈C#就是在谈.NET Framework或者更准确一点是CLR因为.NET Framework除了CLR还包括BCL谈.NET FrameworkCLR也就是在谈C#。因为支撑C#语法之后的就是整个CLR的机制。因此我说C#性能不好和说CLR性能不好说的是一个事情就像说Java性能不好就是说JVM性能不好一样。我不希望在我下面说C#某个地方性能不好的时候再有论者立即指出来“那不是C#的问题那是CLR的问题或者.NET Framework的问题”——如果对C#和.NET还停留在这个认识上请先去读读Jeffrey Richter的《CLR via C#》一书再来看我下面的文章。另外我说C#性能有问题仅针对C#而言与我对其他语言的态度无关。我既不是Java的支持者因为Java的性能比C#还慢也不是C的支持者C太过臃肿复杂也不是C的支持者没有基本的面向对象抽象和垃圾回收。我既不喜欢任何语言也不讨厌任何语言。编程语言在我只是一个工具——我只是希望这个工具是把锋利的牛刀而不是把功能齐全的瑞士小刀。最后我不是毫无选择地反对“新功能”我反对的是“添加的功能、没有重大抽象意义却带来性能损失”如果有“提高性能的新功能”——比如并发编程或者“对管理软件复杂度”有重大意义同时性能损失很小很小——比如面向对象那我举双手赞成。”在理清了前面几个基本点之后下面开始来针对我前文说过的一些问题一一“讲原理”。这篇文章中我首先来剖析反射的性能问题。反射的两大类性能问题【一】反射绑定与调用——使用反射带来的性能问题反射的绑定与调用性能差我想大概做过.NET开发的人都不会怀疑这一点。但是我还是希望那些严肃的程序员认真看看微软CLR程序经理Joel Pobar在MSDN上的这篇文章Dodge Common Performance Pitfalls to Craft Speedy Applications http://msdn.microsoft.com/en-us/magazine/cc163759.aspx清楚理解反射绑定与调用的效率到底为什么那么差有多差差在哪里限于篇幅关系我简单在这里总结一下反射绑定与调用的性能问题具体原理大家参照MSDN这篇文章首先要经过一个绑定过程非常耗时用字符串名称和metadata里面的字符串进行比对字符串查找的算法大家都知道是很慢的操作然后要进行参数个数、类型等的校验如果不匹配还要搜索可能的类型转换进行CAS代码访问安全的验证看允不允许调用。以上几个工作如果不用反射应该是由C#编译器负责在编译时检查的。但是现在如果用反射全都放到了运行时检查。这其中会产生一大堆的临时对象比如MemberInfo Cache给垃圾收集器造成巨大负担纵然有一些对反射绑定和调用的cache优化策略Joel Pobar在这篇文章中给的最大的建议还是能不用反射则不用反射因为性能成本太高。结论反射调用的性能成本很高参见msdn文章中中图2 Relative Performance of Invocation Mechanism。我想这些性能问题大家都会认可。但有些朋友会说“我.NET程序中用反射的很少啊”首先且不论你用的少不少但是微软开发的很多Application Framework对反射的使用现在越来越多比如大量使用反射“绑定与调用”的例子注意是大量不是一点点WPF和Silverlight中的XAML序列化反序列化依赖属性数据绑定ASP.NET MVC中路由、控制器视图等的匹配查找反射绑定和调用反射调用WCF分布式通信中大量的实例激活方法调用序列化与反序列化WF中大量的工作流流程激活、控制、调用………..上面几乎把.NET平台的主要应用框架都包括了不用再举更多例子了吧谁能脱离这些应用框架去写程序所以说你用反射用的少并不代表你最后做出的软件用反射的少你的软件的代码不可能全都是自己写的很多都是依附于微软的Application Framework只要这些Application Framework很重地使用了反射那么你的软件也就很重的使用了反射但有朋友会立即指出“我不用WPF/SL不用WCF、不用WF、不用ASP.NET MVC类库都是自己写代码全都是自己写保证反射用的很少甚至确保压根没有使用反射这些性能负担不久没有了吗”这个问题很好 也是前面谈到.NET各种功能带来的性能问题的时候很多朋友最喜欢的辩词——不用它不就是了嘛首先如果有这样的C#程序员我定佩服你如滔滔江水…….但是我这里要告诉大家的事实是“即便你程序中确实所有的代码都不使用反射由于C#/.NET内置地支持反射那么你也要为此付出性能代价而且是很高的性能代价”。这是本文的重点甚至是我后续很多论战文章的重点——很多C#/.NET机制不管你用不用它只要内置支持这种机制就不可避免要付出性能代价当然如果你要用它还有更多性能代价。好下面让我们来谈谈为什么即便不用反射也要付出很高的性能代价这也是MSDN那篇文章所刻意回避的话题。【二】反射背后需要的支撑机制元数据的性能问题——不使用反射的性能问题要谈这个问题首先大家应该清楚C#/.NET中反射的功能是由metadata来支持的即便你所有的代码中、你用的所有Application Framework的代码中都没有使用一点反射的APIC#编译器还是会在最后生成的EXE或者DLL中生成所有的metadata。如果这个不清楚请先读Jeffrey Richter的《CLR via C#》一书。而 Metadata就是C#/.NET性能的罪魁祸首要理解这一点大家先来做两个简单的针对metadata的分析。. 用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%。真正传统的CodeData总共占用大约1.87M占总大小约37.8%。MSCorlib.dll总共大小4.95M为了支持反射需要添加的元数据竟然有3.01M占到60%的大小我想大家已经看出问题来了。有些朋友可能会说这是特例吧别的DLL呢. 我们再来随便找一个DLL比如WPF的DLLC:\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左右的原因我想这个铁的事实是“老赵们”无论如何都不能否认的。但是“老赵们”的典型言论马上又来了不就是程序有点大吗现在大硬盘很便宜运行起来还是很快的就是.NET Framwork有点大客户安装起来不方便大只是空间效率不影响程序的时间效率这些调调显然都是没有真正搞过“性能优化”的“老赵们”的浅见。空间效率并非对时间效率没有影响而是有致命影响。一个100M的应用程序运行起来肯定要比一个40M的程序慢许多。理由如下程序EXE/DLL最后都是要加载到内存中运行的不是光放在硬盘上的——这也是为什么.NET程序占用内存都超多占用内存多的程序运行起来必然慢。因为内存大的程序必然会出现较多的page fault即换页错误cache missing即缓存失效简单来说要尽可能在CPU缓存中操作working setCPU缓存装不下就要跑到主存里面找主存装不下就要跑到虚拟内存也就是硬盘里面找那样软件运行的性能代价非常高. Page fault和cache missing已经成为现代软件性能的一大公害。很多程序慢下来如果不是蹩脚的算法Page fault和cache missing往往都是罪魁祸首关于这方面的理论很多牛人都专门讲过国外也有比较牛叉的咨询公司专门做这方面的优化大家如果想深度理解这方面可以参照a. CACHE MEMORYIMPLEMENTATION ANDDESIGN TECHNIQUEShttp://www.faculty.iu-bremen.de/birk/lectures/PC101-2003/07cache/cache%20memory.htmb. Improving Managed Code PerformanceWorking SetConsiderationshttp://msdn.microsoft.com/en-us/library/ff647790.aspx#scalenetchapt05_topic33c.以及微软的.NET性能经理Rico Mariani在这里的文章My mom doesnt care about spacehttp://blogs.msdn.com/b/ricom/archive/2004/03/15/89934.aspx所以总结下来就是Metadata非常占用空间一般占到整个EXE/DLL总大小的50%~70%高昂的空间成本会由于Page fault和cache missing等因素转嫁为高昂的时间成本即便在代码中不写一行反射调用代码所有的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.7M3倍多软件size大没关系你要大在地方比如因为功能原因code多一些导致size大我接受。但是你50%-70%的size都去装metadata了而我又不怎么用metadata反射你还要这么大放在那里极大地损害软件性能。这还是一个小小paint玩具软件你让QQ、photoshopoffice等软件用C#/.NET开发试试除非是“老赵们”自己开公