C#会重蹈覆辙吗?系列之2:反射及元数据的性能问题

 

理清几个基本点

在开始谈论性能问题之前,有必要首先理清几个基本点。我们谈C#,就是在谈.NET Framework(或者更准确一点是CLR,因为.NET Framework除了CLR还包括BCL);谈.NET Framework(CLR),也就是在谈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这篇文章):

  1. 首先要经过一个绑定过程,非常耗时(用字符串名称和metadata里面的字符串进行比对,字符串查找的算法大家都知道是很慢的操作)
  2. 然后要进行参数个数、类型等的校验;如果不匹配还要搜索可能的类型转换
  3. 进行CAS代码访问安全的验证,看允不允许调用。
  4. 以上几个工作,如果不用反射应该是由C#编译器负责在编译时检查的。但是现在如果用反射,全都放到了运行时检查。
  5. 这其中会产生一大堆的临时对象(比如MemberInfo Cache),给垃圾收集器造成巨大负担
  6. 纵然有一些对反射绑定和调用的cache优化策略,Joel Pobar在这篇文章中给的最大的建议还是:能不用反射,则不用反射,因为性能成本太高。
  7. 结论:反射调用的性能成本很高(参见msdn文章中中图2 Relative Performance of Invocation Mechanism)。

我想这些性能问题,大家都会认可。但有些朋友会说“我.NET程序中用反射的很少啊?”,首先且不论你用的少不少,但是微软开发的很多Application Framework对反射的使用现在越来越多,比如大量使用反射“绑定与调用”的例子(注意是大量,不是一点点!):

  1. WPF和Silverlight中的XAML序列化-反序列化,依赖属性,数据绑定
  2. ASP.NET MVC中路由、控制器,视图等的匹配查找(反射绑定)和调用(反射调用)
  3. WCF分布式通信中大量的实例激活,方法调用,序列化与反序列化
  4. WF中大量的工作流流程激活、控制、调用
  5. ………..上面几乎把.NET平台的主要应用框架都包括了,不用再举更多例子了吧?谁能脱离这些应用框架去写程序?

所以说,你用反射用的少,并不代表你最后做出的软件用反射的少(你的软件的代码不可能全都是自己写的,很多都是依附于微软的Application Framework,只要这些Application Framework很重地使用了反射,那么你的软件也就很重的使用了反射)

 

但有朋友会立即指出“我不用WPF/SL,不用WCF、不用WF、不用ASP.NET MVC,类库都是自己写,代码全都是自己写,保证反射用的很少,甚至确保压根没有使用反射,这些性能负担不久没有了吗?”这个问题很好! 也是前面谈到.NET各种功能带来的性能问题的时候,很多朋友最喜欢的辩词——不用它不就是了嘛!

 

首先如果有这样的C#程序员,我定佩服你如滔滔江水…….但是,我这里要告诉大家的事实是,“即便你程序中确实所有的代码都不使用反射,由于C#/.NET内置地支持反射,那么你也要为此付出性能代价,而且是很高的性能代价”。这是本文的重点,甚至是我后续很多论战文章的重点——很多C#/.NET机制,不管你用不用它,只要内置支持这种机制,就不可避免要付出性能代价(当然如果你要用它,还有更多性能代价)。

 

好,下面让我们来谈谈为什么,即便不用反射也要付出很高的性能代价?(这也是MSDN那篇文章所刻意回避的话题)。

 

 

【二】反射背后需要的支撑机制:元数据的性能问题——不使用反射的性能问题

 

         要谈这个问题,首先大家应该清楚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#性能也不错,而且写得头头是道,似乎很有道理啊。到底谁说的对啊?

 

这样的疑问很正常,这些论调就是我前文说的“只见树木,不见森林”。为了理清网友的疑问,我在下面的小节中针对这些“一叶障目”的观点进行一一戳穿,以便于大家今后明辨是非。

 

 

几种典型的错误的性能论调或方法

 

1.  函数计时论

要比较性能吗?那好我们写一段函数,用一个时间计数器,在函数执行开始处记录下时间,在函数执行结束前记录下时间,最后一减得到的时间差,同样的功能,哪个语言(或者哪种方式)用的时间少,哪个语言(或者哪个方式)用的时间多,性能差别,一目了然。多客观啊!!!

 

比如,老赵曾经在这篇博文中:一个简单的性能计数器:CodeTimer http://www.cnblogs.com/jeffreyzhao/archive/2009/03/10/codetimer.html 抄袭.NET技术大会上Jeffrey Richter老人家show的性能计数器。

 

然后下面这两篇文章都是用这种“函数计时论”:

《C# vs C++ 全局照明渲染性能比试》: http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html

《回firelong之C#慢》 http://www.cnblogs.com/sumtec/archive/2010/06/22/1762564.html

 

问题是这种做法真的全面、客观的反映了编程语言的性能了吗???用这种办法你可以说某一段C#代码性能还凑合(比如《C# vs C++ 全局照明渲染性能比试》一文中的实验结果,比C/C++差也就20~30%嘛,差的不多嘛!),但是问题是,这就是它们性能差别的全部真相吗?

 

函数记时论,测量的只是某一个微观代码段的性能。不是一个软件的总体性能。比如“函数记时论”就常常忽略掉我们前面metadata所带来的高额的“空间成本”和“时间成本”。正规公司,只要是care性能的,对于性能评测都有一个系统的、全面的、完整的过程(比如在我们公司称作Performance Process,和单元测试、重构、等都作为一个严肃的软件开发过程中的一个环节而存在),会借助一些系统性的工具:比如Compuware的Application Performance Management Solutions:参见这里:http://www.compuware.com/solutions/application-performance-management.asp来做一些系统性的评测报告。不是拿个CodeTimer这样的玩具输出几个时间值,就拍脑袋下结论的。

 

函数计时论经常在各种技术社区中,吵架时展示的tricky demo中用于比较性能,但是放到一个正规公司的严肃项目里面,绝对不会使用这种方法来评估一个编程语言,平台,或者软件的性能。

 

我希望 “老赵们”以后不要再拿CodeTimer这种玩具说事,要真全面比较性能,用Compuware的Application Performance Management Solutions一整套工具和过程来比较整个软件的性能,而不是某一段微观代码的性能。

 

 

2.  性能选择论

某个功能影响性能,你不用不就没影响了吗?又没有人逼你用!

 

前面已经证明,C#/.NET的反射功能,你哪怕一点也不用,也有很大的性能成本(即:代码中完全不用反射,为了支持反射的metadata带来的空间成本和时间成本也非常高昂)。所以希望以后“老赵们”不要再说这样的话。

 

3.  损失忽视论

这个功能带来的性能损失是很小的,可以忽略不计。

 

性能是一个软件最核心的使用指标——如果一个软件性能不行,就是差软件!没有哪些个性能损失是可以忽略不计的。因为在程序代码中,任何一个性能损失点,都有可能因为各种因素被放大(比如长循环,大规模并发用户等)。

 

“老赵们”喜欢写“性能不咋地的高级企业应用”,然后忽悠客户加硬件。但是请不要忽悠整个.NET社区的程序员以为天下的软件都是“很高级的企业应用”。

 

4.  性能垫背论

 

“Java的这个feature性能比C#的差,所以C#这个feature性能好”——C#的某些feature(比如反射)性能比Java好,但并不能说明这个feature本身没有性能问题(这只能说明Java在这个上面性能太差,说明不了C#性能好)。

 

请“老赵们”以后不要天天在.NET社区里说“C#这个比Java好,那个比Java cool”,这就像天天告诉自己的孩子,你比你们班最后一名的那个孩子好多了,你说孩子还能学好吗???你怎么总拿C#跟差的比,不跟好的比呢?

 

 

最后结语

 

好,文章写完了,我希望.NET技术社区的“老赵们”围绕“反射的性能话题”来辩驳,不要扯别的话题来放烟雾弹(C#/.NET中别的技术话题,我会在下面的文章中一篇一篇来讨论,请大家耐心等待给我一点时间)。谢谢!

 

正要贴本文的时候,看到《关于C#开发山寨操作系统,程序语言,浏览器,IDE,Office,Photoshop等大型程序的可行性歪论及意义》http://www.cnblogs.com/DSharp/archive/2010/06/24/1764210.html 这篇文章。我的回答非常明确:没有任何可行性,且不论商业可行性、其他技术问题,光反射一项带来的两大性能负担就把路堵死了——这也是我前文说的那么多软件为什么不采用C#开发的一个关键原因——你搞一个100M的程序,中间有50M都是metadata,你还让人程序活下去吗?(记住,50M不仅仅是空间成本,带来的时间成本照样很大!)

 

P.S. 本文中的“老赵们”指的是那些天天拿着C#语言新特性耍酷表演、而不研究真实技术问题的“所谓的技术精英们”,并不特指老赵一个人,或者老赵的每一个阶段(老赵有一段时间还算在研究真问题)。请不要对号入座,谢谢!

 

posted on 2010-06-24 17:42  firelong  阅读(17698)  评论(342编辑  收藏

导航