理清幾個基本點
在開始談論效能問題之前,有必要首先理清幾個基本點。我們談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這篇文章):
- 首先要經過一個綁定過程,非常耗時(用字串名稱和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的代碼中都沒有使用一點反射的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#語言新特性耍酷表演、而不研究真實技術問題的“所謂的技術精英們”,並不特指老趙一個人,或者老趙的每一個階段(老趙有一段時間還算在研究真問題)。請不要對號入座,謝謝!