在本書的第一部分,我講述了C#語(yǔ)言的許多基本方面——包括整型、字符型和浮點(diǎn)型之間的轉(zhuǎn)換,循環(huán)、迭代算法,以及字符串比較、拼接和 特定符號(hào)統(tǒng)計(jì) 。正如我所說(shuō),對(duì)一種新的語(yǔ)言與它的類似語(yǔ)言進(jìn)行全面、公平的性能比較,范圍將會(huì)很龐大,所以本書沿用第一個(gè)例子并只對(duì)有限的重要的部分特性做測(cè)試。這種比較,以先前的 C#及它的庫(kù)與C、C++、D和Java的特性對(duì)比為基礎(chǔ),主要在以下幾個(gè)領(lǐng)域:
·類機(jī)制(封裝與模板)
·異常
·內(nèi)存— 重點(diǎn)
·大型文件的隨機(jī)訪問(wèn)
·資源獲取初始化
正如我在第一部分提到的,這里所做的測(cè)試只是針對(duì)程序性能的一小部分,硬件及操作系統(tǒng)的配置是測(cè)試語(yǔ)言 /編譯器/庫(kù)函數(shù)的機(jī)器的典型配置:2-GHZ,512-MB,Pentium IV,Windows XP Professional。 這里我們看到的任何結(jié)果揭示了在其它環(huán)境下可能的性能,這不說(shuō)明可以假設(shè)這些結(jié)果就是正確的典型例子。 最后要說(shuō)明的是,當(dāng)我提到“C#比Java好”等結(jié)論的時(shí)候,一定是指“在特定的測(cè)試環(huán)境的條件下”,這一點(diǎn)請(qǐng)你銘記在心。
性能評(píng)價(jià)
所有的代碼還是用和第一部分一樣的編譯器編譯。 C#代碼在.NET框架 1.0.3705(可從http://microsoft.com/netframework/免費(fèi)下載)下,由Visual C#.NET 編譯器v7.00.9466編譯。D代碼由Digital Mars D 編譯器 Alpha v0.62 (可從http://www.digitalmars.com/免費(fèi)下載) 編譯。Java代碼由J2DKSE1.4.1-02 ( 可免費(fèi)下載 http://java.sun.com/ ) 編譯。 C和C++代碼 ( 單線程 , 包括 raii , 和靜態(tài)鏈接庫(kù) ) 由使用 STLPort v4.5的Digital Mars C/C++ v8.33 ( 可從 http://www.digitalmars.com/免費(fèi)下載 ) 和使用 Visual C++ 6.0的頭文件和庫(kù)文件的Intel C/C++ v7.0來(lái)編譯。所有這些編譯器均使用最優(yōu)化處理 ( set for speed ) ; Intel 用-QaxW flag [2]提供兩套編碼路徑:一個(gè)是針對(duì)有SSE2的Pentium IV處理器,另一個(gè)是針對(duì)其他所有的處理器——在運(yùn)行時(shí)選擇合適的一個(gè)。(因?yàn)镮ntel編譯器只適用于Intel處理器構(gòu)造,特別是在這個(gè)測(cè)試中的Pentium IV,這編譯器的編譯結(jié)果必須在此基礎(chǔ)上評(píng)價(jià),不能在其他的處理器上進(jìn)行同樣的過(guò)程,比如Athlon,甚至是舊版本的Intel處理器也不行。)
后面將要描述我們測(cè)試的假想的五種例子中的性能表現(xiàn)。他們都用2-GHZ,512-MB,Pentium IV,Windows XP Professional的配置,并沿用在第一部分用的性能分析系統(tǒng)。每一種語(yǔ)言所編程序的調(diào)試都執(zhí)行7次,同時(shí)沒(méi)有其他占線過(guò)程,去掉最高速的和最低速的,紀(jì)錄平均值。所有的時(shí)間信息,通過(guò)僅區(qū)分相應(yīng)時(shí)間區(qū)間的高性能計(jì)時(shí)器保存下來(lái)。(C用戶調(diào)用Win32 API QueryPerformanceCounter() ;C++ 用 Win-STL ' s performance_counter [4], 可在http://winstl.org/找到 ; C# 用 SynSoft.Performance.PerformanceCounter ,可在 http://synsoft.org/dotnet.html找到; D 用 synsoft.win32.perf.PerformanceCounter , 可在http://synsoft.org/d.html找到;Java 用 System.currentTimeMillis() .)每一個(gè)測(cè)試程序都包含一個(gè)啟動(dòng)迭代的程序,這樣可以將啟動(dòng)或緩沖時(shí)的影響最小化。
在每一個(gè)例子中的C#,C++和D的特征: C在所有 保存 封裝和 raii( 類)的地方出現(xiàn),因?yàn)樗鼪](méi)有這兩種例子必需的語(yǔ)言特征。 raii 沒(méi)有出現(xiàn)Java,因?yàn)镴ava甚至連 Resource AcquisitionIs Initialization (RAII [1])的機(jī)制也沒(méi)有,所以它僅僅作為在GC-kicking的一個(gè)練習(xí)。簡(jiǎn)單的說(shuō),我不會(huì)展示所有的語(yǔ)言的每一個(gè)例子的所有特性,但是所有這些都包含于在線記錄中(連同產(chǎn)生他們的Digital Mars make-compatible make-file)。
下面就對(duì)測(cè)試性能的例子進(jìn)行評(píng)價(jià) :
封裝 。這個(gè)例子( Listing1)的目的是比較不同語(yǔ)言(除了C)在類機(jī)制上的代價(jià),這些語(yǔ)言并不完全支持相對(duì)于它們所做的所有類型的一般處理。尤其是C#和Java一般不能存儲(chǔ)和使用它們的內(nèi)置類型實(shí)例,比如int,long,bool等等,同樣,對(duì)于從這些實(shí)例派生的類型也不行。相反C++和D都支持模板,它可以給任何類型的一般性操作帶來(lái)方便。在以后的文章里,我將詳細(xì)地講解容器。但在這個(gè)章節(jié)將不會(huì)用到它,如果提及它反而會(huì)分散對(duì)于封裝意義的理解。C#和Java中未封裝的變量傳遞ints給異或函數(shù),封裝變量傳遞整形值在Int32(C#)和Interger(Java)的實(shí)例中。注意Java不支持模糊的封裝(這被證明是好的因?yàn)榫幾g器將對(duì)于這種封裝帶來(lái)的低效給予警告),所以實(shí)例必須明確地被創(chuàng)建(在列表1中標(biāo)記“Java ” 處可以看到)。 C++和D的運(yùn)行是簡(jiǎn)單的使用模板,這就是它們的泛型機(jī)制。(在這里我不對(duì)“一切都是對(duì)象”的模板的好處作深入探討,但是我將在這期的姊妹篇Stepanov camp中對(duì)它做詳細(xì)講解)。
異常處理 。異常的語(yǔ)義在各種語(yǔ)言之間有很大的區(qū)別。 在 C++,異常處理和調(diào)用析構(gòu)函數(shù)一起沿著堆棧操作下來(lái),然而在有垃圾回收的語(yǔ)言中,這些對(duì)象被丟棄,直到將來(lái)的某一時(shí)刻被收集。 這樣,就無(wú)法對(duì)各種語(yǔ)言進(jìn)行有意義的比較。所以在這個(gè)例子中,只能通過(guò)將一段 using/aware代碼與另一段沒(méi)有異常處理支持的相同代碼進(jìn)行比較。這個(gè)例子有三種測(cè)試,它們的目的就是去衡量使用和拋出異常處理的代價(jià)。第一(Listing 2)是比較使用異常處理和通過(guò)函數(shù)返回值來(lái)傳遞信息給調(diào)用者這兩者的代價(jià)。第二(Listing 3)是評(píng)估在try-catch模塊中執(zhí)行的代價(jià)(盡管沒(méi)有異常處理被拋出)。最后(Listing 4)就是評(píng)估遍歷執(zhí)行try-catch模塊的代價(jià)(同樣,這沒(méi)有異常處理被拋出)。
Mstress。 這一個(gè)例子的目的是,在假設(shè)分配比回收優(yōu)越的基礎(chǔ)上,看看垃圾回收語(yǔ)言是否在高的內(nèi)存利用率上有重要的非線形關(guān)系。這里存在四種測(cè)試,每一個(gè)都牽涉到分配與釋放( C 和 C++)或者丟棄(C#, D, 和 Java)在一個(gè)時(shí)間段內(nèi)的內(nèi)存塊。這些塊大小是通過(guò)相同的偽隨機(jī)數(shù)算法生成的(詳見文章的第一部分),這能使塊的大小處于1至1000之間,從而在確保每種語(yǔ)言在相同的環(huán)境下測(cè)試時(shí)仿效現(xiàn)實(shí)世界中的分配。第一種測(cè)試(Listing 5)通過(guò)一個(gè)不斷循環(huán)的變量分配了1000個(gè)內(nèi)存塊,目的是看看頻繁地分配會(huì)不會(huì)“留下”更多的碎片,這些碎片會(huì)阻礙分配的進(jìn)行直到突然產(chǎn)生明顯的GC收集。第二種測(cè)試(Listing 5)增加了1000次循環(huán)中分配的次數(shù),以測(cè)試由不斷增加的內(nèi)存負(fù)擔(dān)引起的性能的 損失 。 第三和第四種測(cè)試(沒(méi)列出來(lái))跟前兩個(gè)相似,它們?cè)黾恿肆硗獾膬?nèi)存塊和分配/回收的交叉活動(dòng),目的是為了模擬不斷增加的內(nèi)存碎片。 C使用malloc()/free(),而C++,C#,D和JAVA用new[]。
rafile 。這一個(gè)例子( Listing 6)評(píng)估各種語(yǔ)言從大文件中隨機(jī)讀取的能力。它利用某種偽隨機(jī)算法(詳見文章第一部分)來(lái)模擬在大文件中的查找/讀取。這里用到了兩個(gè)文件,一個(gè)4.2MB,一個(gè)21.1MB。在每一個(gè)查找點(diǎn),四個(gè)字節(jié)被讀取,并且和一個(gè)運(yùn)行值進(jìn)行異或運(yùn)算(這樣有利于證明所有的語(yǔ)言以相同的方式查找和讀?。?。有意思的是,原始的運(yùn)行程序讀了32位的整數(shù),但不幸的是,JAVA用了network byte-order而不是host byte-order,這與C, C++, 和D的machine-order reading是不相容的。
Raii(類) : 這最后的例子目的是為了評(píng)估沒(méi)有內(nèi)建支持這個(gè)非常重要的語(yǔ)言特征(很不幸 C#和Java都沒(méi)有)而付出的代價(jià)。此例子表達(dá)了通過(guò)資源句柄XUser 類型從XFactory資源管理器分配X資源到客戶端代碼(見代碼段7)。在C++和D語(yǔ)言中,XUser 提供一個(gè)析構(gòu)函數(shù),在析構(gòu)函數(shù)里分配的資源自動(dòng)地回收到資源庫(kù),資源在這種情況下是個(gè)信號(hào)量,.NET和D的執(zhí)行代碼可從 http://synsoft.org/dotnet.html 和 http://synsoft.org/d.html 下載。C#、D和Java 提供它們自己抽象的同步機(jī)制,但我需要一個(gè)信號(hào)量去引起交叉線程的競(jìng)爭(zhēng),去保證同樣的同步機(jī)制以便所有語(yǔ)言的進(jìn)行生動(dòng)的比較。此信號(hào)量在每種語(yǔ)言執(zhí)行代碼中都有最初可獲得的五個(gè)鍵值。
盡管 C#的XUser 類有一個(gè)析構(gòu)函數(shù),但此析構(gòu)函數(shù)僅在實(shí)例進(jìn)行垃圾碎片收集時(shí)被調(diào)用。(C#使用析構(gòu)函數(shù)的語(yǔ)法,但編譯器把它翻譯成 CLR (公共語(yǔ)言運(yùn)行時(shí)刻)的 Finalize() 方法。)這種情況跟 Java一樣。C# 還盡量更多地支持習(xí)慣用語(yǔ)(雖然它支持的還不夠),這是通過(guò)使用引用聲明來(lái)實(shí)現(xiàn)的,正如表7所示。(為了方便,“using-ed”類型必須實(shí)現(xiàn) IDisposable 接口,否則聲明無(wú)效),一個(gè)明顯的問(wèn)題是類的使用者必須承擔(dān)確保語(yǔ)義的正確的負(fù)擔(dān),而不是類的開發(fā)者。這是奇怪的二分法:C#是面向?qū)ο蟮?,但它依靠類的使用者?duì)類的內(nèi)部執(zhí)行代碼的足夠理解以決定是否需要此類的直接析構(gòu)。C++和D語(yǔ)言過(guò)程明確,由類自己決定:C++通過(guò)析構(gòu)函數(shù)的執(zhí)行,D提供析構(gòu)或終止函數(shù),類聲明驗(yàn)證關(guān)鍵字 AUTO (沒(méi)有這些,它按C#和Java的方式運(yùn)行正常的垃圾碎片收集程序)。
C#語(yǔ)言使用 USING 聲明(和 IDisposable 接口)的一個(gè)可替代方法是依靠垃圾回收。在形成這個(gè)情況之前,我假設(shè)當(dāng)內(nèi)存使用率達(dá)到觸發(fā)閥值或者主線程空閑/等待的時(shí)候啟動(dòng)垃圾碎片回收。(我不知道這種誤解是怎么形成的,或許只是異想天開)。當(dāng)我在執(zhí)行 Raii2 例子的最初版本的時(shí)候(變量沒(méi)有使用 USING 聲明),處理進(jìn)程馬上掛起。很明顯,在其他線程堵塞時(shí)垃圾碎片收集沒(méi)有觸動(dòng),只有在內(nèi)存消耗到一定程度才觸動(dòng)。因此,為了程序執(zhí)行,我們被迫在另一個(gè)工作線程中自己剔除垃圾碎片收集。當(dāng)然,這在實(shí)際編程中是非??尚Φ膱?zhí)行策略,執(zhí)行結(jié)果證明了這一點(diǎn)。
結(jié)論
大部分的結(jié)果是以絕對(duì)運(yùn)行時(shí)間數(shù)(全部循環(huán)或每循環(huán))或者 C#的執(zhí)行時(shí)間和C,C++,D,Java分別的百分比來(lái)表達(dá)的。在后一種方式,圍繞著百分刻度的結(jié)果表示C#的性能比率,高的值表示C#的性能優(yōu)越,低的值表示相對(duì)低下。 Raii 的情況是個(gè)例外,在這里結(jié)果是以C++(Digital Mars)時(shí)間的百分比表示的,由于垃圾碎片收集代碼的消耗,調(diào)用時(shí)間值是以對(duì)數(shù)刻度表示的。這說(shuō)明在對(duì)數(shù)比例上歸根于非常高的垃圾處理花費(fèi).
![]() |
BOX . 從圖 1中我們能看出,在合理范圍里(C#語(yǔ)言的97%-271%)對(duì)于所有編譯語(yǔ)言解除封裝操作(就簡(jiǎn)單整形數(shù)據(jù))成本的差別:最差的是C++(Digital Mars),成本是C#的271%,最好的是C++(Intel) 成本是C#的97%.假設(shè)執(zhí)行相對(duì)比較簡(jiǎn)單的 異或 運(yùn)算, C#幾乎可以像 Intel那樣出色,你對(duì)這個(gè)并不會(huì)感到很驚奇,但你相信C#要比C++(Digital Mars)和D模板實(shí)例快兩倍.我有些驚訝C++(Digital Mars)這個(gè)相對(duì)弱的性能,特別是和D比起來(lái)它要慢得多,然而他們出自同一開發(fā)商。
顯然這個(gè)封裝花費(fèi)是有意義的 ,相當(dāng)于C++模板花費(fèi)4-10倍.這個(gè)測(cè)試僅僅包括整數(shù),相對(duì)來(lái)說(shuō)可能不能完全反映特殊類型.雖然如此,我們能有把握斷言這個(gè)模板輕易贏得了性能戰(zhàn).有趣的是就他們各自的成熟度而言,C#要比java稍好,
差距并不大 (小于4%).(我想可能由于.NET處理目標(biāo)JIT3?!?】)。更有趣的是,事實(shí)上在未封裝的形態(tài)下,c#明顯比java快.
除了圖 1以外,有人認(rèn)為,不拋出的異常variants都是是比拋出的異常副本要快的(如圖2). 用 c和c++,Intel完成任務(wù)要比所有其它不拋出的異常variants快.c#緊隨其后。似乎c,c++,D(不考慮編譯器)在異常處理上花費(fèi)大致相等,符合win32異常處理機(jī)制的預(yù)期限制因素.我們能看到三種語(yǔ)言在拋出異常variants上的差別是比較小的,很可能的原因是由于有相關(guān)的額外的管理消耗關(guān)系 (c++規(guī)定執(zhí)行破壞功能的堆棧是從捕獲點(diǎn)到拋出點(diǎn)的來(lái)回).
我發(fā)現(xiàn)有趣的是在拋出異常variants方面c#和java的執(zhí)行是有關(guān)系的.
因?yàn)檫@兩種語(yǔ)言語(yǔ)義在異常拋出處理要?jiǎng)儆诜祷禺?dāng)前錯(cuò)誤值 ,也勝于其它語(yǔ)言,
尤其 c語(yǔ)言所呈現(xiàn)的關(guān)系執(zhí)行調(diào)用.我非常贊同Brian Kernighan和Rob Pike
(<>,Addison Wesley,1999)所說(shuō)的例外僅僅在例外條件的使用,并不是必然會(huì)發(fā)生的.然而人們能容易看到為何服務(wù)器在處理巨大數(shù)量的數(shù)據(jù)的時(shí)候可能出現(xiàn)的例外,而經(jīng)常擔(dān)心性能的問(wèn)題.正是如此,java以小于其它語(yǔ)言25%的消耗給人深刻的印象.由此我們限定win32,采用兩個(gè)有代表性的差不多的c編譯器來(lái)構(gòu)造例外處理,雖然沒(méi)有定論,但是我認(rèn)為java使用了不同的機(jī)制.
那么c#的異常拋出性能如何呢?在相關(guān)條件下,相對(duì)其他語(yǔ)言而言,使用異常處理語(yǔ)句是不使用異常處理語(yǔ)句的21-77倍,而c#則是191倍;鑒于Java的出色表現(xiàn),.net工具應(yīng)該引起注意了。
Except-2. 這個(gè)圖表是為了弄清楚在一個(gè) try-catch范圍內(nèi)異常處理語(yǔ)句的執(zhí)行是否耗時(shí),如圖3,大多數(shù)語(yǔ)言的表現(xiàn)同預(yù)期的一樣:c、c++和D對(duì)于Digital Mars是類似的;Intel c 和c++比其它都好.c#要比java快.相比較就性能而言,
c#是很優(yōu)勢(shì)的,令人感興趣的是,c#在進(jìn)行異常處理是耗時(shí)比不進(jìn)行時(shí)還要少.產(chǎn)生這種結(jié)果肯定是有原因的(這個(gè)結(jié)果是經(jīng)過(guò)了多次驗(yàn)證的), 但由于對(duì)于c#異常處理機(jī)制我沒(méi)有很深的認(rèn)識(shí),我不能做出合理的解釋.同樣,雖然并沒(méi)有定論,對(duì)于D來(lái)說(shuō),是否進(jìn)行異常處理對(duì)結(jié)果似乎并無(wú)影響!.
我認(rèn)為這是因?yàn)樗挥们宄龡?nèi)數(shù)據(jù), java也有大致相同的自由度,并且表現(xiàn)出的差異也并不大。
except-3 如圖表 4所示,測(cè)試結(jié)果與預(yù)期大體相符。除了D編譯器的結(jié)果中微不足道的差異,所有的語(yǔ)言在遍歷異常處理語(yǔ)句的時(shí)候都比不進(jìn)行是耗時(shí)要長(zhǎng),盡管如此,我仍然對(duì)在這兩種情況下差異如此之小感到吃驚。我們通常認(rèn)為使用異常處理機(jī)制會(huì)耗費(fèi)時(shí)間,并且我認(rèn)為遍歷異常處理語(yǔ)句會(huì)在啟動(dòng)和完成上耗費(fèi)大量時(shí)間;事實(shí)上,我一開始就預(yù)期兩者的差異將會(huì)非常地顯著。然而,在這個(gè)測(cè)試當(dāng)中,兩者的差異相當(dāng)不明顯。
但是這個(gè)結(jié)果并不難理解,導(dǎo)致差異如此小的原因首先在于:這次測(cè)試太過(guò)于純粹。這次測(cè)試的所有異常處理(類型)都是 int型的。盡管并沒(méi)有異常被拋出,這樣做的目的在于避免在異常處理體的構(gòu)造(和析構(gòu))上耗費(fèi)時(shí)間和消除各編譯器處理機(jī)制不同所造成的影響。其次,本次測(cè)試中沒(méi)有基于框架的結(jié)構(gòu)體,因?yàn)樵贑++中,盡管析構(gòu)函數(shù)并不會(huì)真正被調(diào)用,析構(gòu)函數(shù)調(diào)用方面的準(zhǔn)備也是必須進(jìn)行的,這樣也會(huì)造成時(shí)間的耗費(fèi)。(注意析構(gòu)函數(shù)無(wú)論異常是否被拋出都會(huì)被調(diào)用。)這些耗費(fèi)都會(huì)造成測(cè)試的不公平,所以我們選擇了int型。注意到程序不論是因?yàn)楫惓伋龆兄惯€是正常退出,析構(gòu)函數(shù)都是要被調(diào)用的。所以它僅僅做為勾子而被添加進(jìn)這些額外的函數(shù)調(diào)用中。然而,我認(rèn)為所有的這些因素并不是很充足,它們僅僅使我們可以從那張表里知道當(dāng)不使用的時(shí)候異常的開銷是非常小的。很自然,這也正確的表明了一個(gè)人對(duì)所有語(yǔ)言性能(perform)的期望,當(dāng)然我們要給它足夠的信任.
metress-1 這個(gè)測(cè)試很清楚的表明了在不使用內(nèi)存分配機(jī)制的展示 (exhibit)中不斷增長(zhǎng)的對(duì)固定數(shù)量的內(nèi)存快(隨機(jī)大小)分配/釋放的循環(huán)的反應(yīng)是非線性的。事實(shí)上,在很大程度上它們從沒(méi)有線性(增長(zhǎng))的趨勢(shì):從性能上來(lái)說(shuō)并沒(méi)有什么變化(見圖5),除了在一些低循環(huán)(low interations)上有所不同之外.(有趣的是,那些低循環(huán)的非線性是有意義的--占到了全部命令的50%還多--當(dāng)然這僅僅是對(duì)c#和java而言).不管內(nèi)存分配機(jī)制是否在每次特循環(huán)結(jié)束后都立即恢復(fù)對(duì)1000個(gè)內(nèi)存塊的釋放,或者僅是簡(jiǎn)單的把它們交給GC在以后的某個(gè)時(shí)間處理,語(yǔ)言/運(yùn)行庫(kù)總是看上去幾乎完全不受這些循環(huán)執(zhí)行的影響.
在這組性能相對(duì)的測(cè)試中,我們可以清楚的看到一些它們之間的不同。在使用 Digital Mars 分配方式的語(yǔ)言中,C和C++的表現(xiàn)是最好的。Visual C++的運(yùn)行庫(kù)比使用Digital Mars的語(yǔ)言低大概2.5-3倍,對(duì)于使用Digital Mars 的語(yǔ)言和Visaul C++運(yùn)行庫(kù)來(lái)說(shuō),C和C++基本上是相同的。最后,很明顯,Java 比C#慢了3-4倍,而比C慢了差不多7倍.
mestress -2
就像我們從圖表中看到的,在一定數(shù)目的分配釋放內(nèi)存的循環(huán)中內(nèi)存塊(隨機(jī)大小)的不斷增長(zhǎng)的反應(yīng)是變量的引用是非線性表現(xiàn)的。這正是我們所希望的,因?yàn)樵诿看窝h(huán)中分配的內(nèi)存總量是按指數(shù)方式增長(zhǎng).每次循環(huán)所分配的內(nèi)存的大小都低于10,000塊.使用Digital Mars分配方式的C 和C++的效率依然是極為優(yōu)秀的。只是效率表現(xiàn)上低于平均值,而這里也不得不提到j(luò)ava,它的表現(xiàn)同樣不好.Visual C++的運(yùn)行庫(kù)(C和C++都適用)相對(duì)于C#,D 和Java來(lái)說(shuō),有一個(gè)不錯(cuò)的開始,但它很快就降到了一個(gè)很低的水平上。
Java在每次循環(huán)是使用10,000內(nèi)存塊之前的表現(xiàn)非常有競(jìng)爭(zhēng)力,并在這一點(diǎn)急劇上升。D在整個(gè)測(cè)試中都幾乎有著一致表現(xiàn).幾乎一直都是非線性的,這與C#很接近。
如果可能的話,我都希望看到一條總夠長(zhǎng)的X軸,C#在內(nèi)存溢出的情況下仍然保持每次循環(huán)不超出10,000內(nèi)存塊的水平并預(yù)防這種情況的出現(xiàn)。(D和Java似乎也做的到,但那也只是類似C#的行為一旦被發(fā)現(xiàn)就中止測(cè)試。)
mstress-3 和 4 從前兩個(gè)測(cè)試(variants)看,除了時(shí)間的的增長(zhǎng),反復(fù)的交叉存取和塊變量在執(zhí)行上的表現(xiàn)并沒(méi)有什么不同。曲線的走勢(shì)或者說(shuō)不同的語(yǔ)言/運(yùn)行庫(kù)的相對(duì)表現(xiàn)在這一點(diǎn)上沒(méi)有什么明顯的改變。
我認(rèn)為C和C++,在使用外部free/delete的條件下,重新使用的新近釋放的碎片。相反的,我很難想像出C#,D和Java 是如何使用垃圾收集機(jī)制周期性尋找每次循環(huán)所分配的內(nèi)存從而盡可能的減少由內(nèi)存碎片所引起的負(fù)面效應(yīng)的,或者在這個(gè)跟本就沒(méi)有產(chǎn)生碎片.除去這些不同,這兩種方式的表現(xiàn)還是很相似的。
這只是一種理想的我們所希望的分配機(jī)制的表現(xiàn) ---畢竟,那是一個(gè)很極端情況,所有內(nèi)存分配都可以在給定的周期內(nèi)完全返回---雖然程序的執(zhí)行都達(dá)到的目的。
rafile. 這次測(cè)試中我所希望是那些語(yǔ)言實(shí)際 (效率)上并沒(méi)有什么不同.除了C++的執(zhí)行也許比其它的低上幾個(gè)百分點(diǎn).
圖表 7中可以看出,C#的在文件的隨機(jī)存取上比C(使用Digital Mars)要好,但低于C++(使用Intel和VC6的連接庫(kù)),和D與Java現(xiàn)表現(xiàn)基本持平。但是C++運(yùn)行庫(kù)表現(xiàn)出令印象深刻的性能。
從所有這一系列測(cè)試來(lái)看,Intel 似乎已經(jīng)能夠產(chǎn)生性能不錯(cuò)的代碼了。但是它的連接的運(yùn)行庫(kù)頭文件卻是Visual C++ 6.0的,這個(gè)(大概)不是由Intel編譯器產(chǎn)生的。因比它幾乎是以壓倒性的性能優(yōu)勢(shì)超過(guò)了DMC,這主要是由于各自的Digital Mars 和微軟Visual C++運(yùn)行庫(kù)的性能。(我承認(rèn)有點(diǎn)吃驚,對(duì)于些測(cè)試結(jié)果正好可以反對(duì)兩個(gè)賣家的名聲--應(yīng)該或是不應(yīng)該得到的)。這也表明一個(gè)人的偏見是非常沒(méi)有理由的。
另一件值的注意的事就是對(duì)不同大小文件訪問(wèn)的開銷都非常的小。這也表明所有的語(yǔ)言都利用操作系統(tǒng)的優(yōu)勢(shì)很輕易的達(dá)到了相同的程度。
raII . 從圖 8中我們能看出使用statement的C#的表現(xiàn)只有C++析構(gòu)函數(shù)效果的一半。D的性能與之相當(dāng),雖然產(chǎn)生的代碼中有錯(cuò)誤(或者是D的Phobos運(yùn)行庫(kù)),當(dāng)進(jìn)程創(chuàng)建的對(duì)像超過(guò)32,000左右的時(shí)候會(huì)使該進(jìn)程掛起,從而防止過(guò)多(一個(gè)以上的)通訊中的數(shù)據(jù)點(diǎn)被確定的使用.在這組單獨(dú)的測(cè)試中我們可以看到RAII對(duì)C#和D的支持是很完善的,但并不如想象中那樣優(yōu)秀。如有你要做很多scoping--并且你想,也許要足夠的robustaness的幫助---你最好還是選擇(stay with)C++。當(dāng)依賴于.NET的垃圾回收機(jī)制時(shí),因?yàn)樘幚磉^(guò)程只是簡(jiǎn)單地被掛起,所以C#的性能比較難以測(cè)算。可是我們不希望.NET經(jīng)常調(diào)用它的垃圾回收線程,但我找不到理由解釋為什么回收線程不以較低優(yōu)先級(jí)在內(nèi)核中等待,當(dāng)有垃圾對(duì)象需要被回收和主線程空閑時(shí)再進(jìn)行有效的回收。當(dāng)然,我在垃圾回收方面不是專家,并且可能有一種合理的理論原因說(shuō)明這為什么不是一個(gè)好的處理方法。
GC(垃圾回收)即使響應(yīng)的更靈敏,我們可以在它的性能結(jié)果上看出效果并不與之匹配。對(duì)應(yīng)第二組數(shù)據(jù)指針的方法表明了GC每隔1ms被觸發(fā)一次。
GC(垃圾回收)的方法比使用使用聲明慢了近千倍,所以這不僅僅是一個(gè)技術(shù)問(wèn)題的范圍。自然,這跟關(guān)于這個(gè)例子的假設(shè)有關(guān),同時(shí)跟幾乎每個(gè)人都用垃圾回收機(jī)制去管理頻繁爭(zhēng)用的資源的釋放這個(gè)不辯的事實(shí)有關(guān),但我沒(méi)預(yù)計(jì)到這個(gè)變量的性能是如此之差。同時(shí)這也給了讀過(guò)Java/.NET書籍的C++程序員對(duì)于書籍建議(或許說(shuō)依靠)使用終止函數(shù)來(lái)清除資源而感覺疑惑的一個(gè)解釋。
結(jié)論
在第一篇文章,我對(duì)于不同的環(huán)節(jié)得出不同的結(jié)論,這些環(huán)節(jié)或是語(yǔ)言本身造成的,或是庫(kù)或者二者造成的,在這里我也這么區(qū)分,因?yàn)檎Z(yǔ)言特征而產(chǎn)生的影響的部分通常涉及:異常,封裝 /模板和RAII的實(shí)現(xiàn)。文件存取部分可以看作直接對(duì)庫(kù)函數(shù)的操作。(沒(méi)有任何一種語(yǔ)言阻止寫替換操作,雖然這樣做并不是很容易)。內(nèi)存管理部分受語(yǔ)言和內(nèi)存的影響――雖然我不清楚可以用哪一種管理機(jī)制去替代C#和JAVA的缺省的內(nèi)存管理機(jī)制,但是對(duì)于其他語(yǔ)言這是很簡(jiǎn)單的。
封裝。當(dāng)我們比較封裝和模板時(shí),我們可以看出模板明顯比封裝出色很多。我相信這不會(huì)讓大家感到很驚奇,但是事實(shí)上封裝消耗的系統(tǒng)資源是模板的十倍。自然,作為模板類庫(kù)的作者( http://stlsoft.org/ ),我得出這個(gè)結(jié)論也許帶有偏見,但是這些數(shù)據(jù)在表示更廣泛的情況時(shí),他們自身就說(shuō)明這一點(diǎn)。在后面,我將說(shuō)到容器和算法,我們將看到比預(yù)計(jì)更多的這樣的結(jié)果。
至于有關(guān)異常情況,我想講四點(diǎn):
1.在所有的程序語(yǔ)言中,使用異常處理從調(diào)用的函數(shù)過(guò)程中返回,而不是保留他們作為異常事件的指示,將導(dǎo)致執(zhí)行花費(fèi)巨大的成本。
2.C#的異常處理機(jī)制相對(duì)于其他被測(cè)試的語(yǔ)言效率是極低的.
3.在有異常處理的上下文環(huán)境下運(yùn)行(比如用try-catch的情況)對(duì)于性能沒(méi)有多大影響,除了C#,它為了提高性能(實(shí)際上,我對(duì)于這種結(jié)果很不理解,并且懷疑這是.NET運(yùn)行期的一個(gè)人為結(jié)果而不是C#/.NET的一般特征。)
4.交叉于異常上下文的執(zhí)行(比如說(shuō)進(jìn)入try-catch和/或 離開try-catch)對(duì)于系統(tǒng)性能的影響基本上是非常小的。
Raii. C#支持的RAII例子比C++在性能方面差,雖然不是很多,但與D相比差別無(wú)幾,基本一致。(這種一致指出了在處理堆分配對(duì)象的確定性析構(gòu)時(shí)的基本限制)然而,從理論觀點(diǎn),或從易用性和/或穩(wěn)健性的實(shí)踐觀點(diǎn)來(lái)看,這里還是有很大差距的。C語(yǔ)言缺乏機(jī)制可以解釋為年代已久,并且它是一種程序語(yǔ)言。很遺憾,java缺乏這種機(jī)制,但是它只是可以解釋為忽視了。(至今為止我們已經(jīng)用java8年左右了,所以“忽視”可能也有些牽強(qiáng)。)C#在這方面的努力還不成熟(因?yàn)樗嗟匾蕾囶惖挠脩舳皇穷惖淖髡撸?,很奇怪的是C#/.NET很多優(yōu)點(diǎn)都是在Java中被看作瑕疵/遺漏的地方,比如屬性,輸出參數(shù),和無(wú)符型。
Mstress. 這個(gè)內(nèi)存測(cè)試的目的是證明如果頻繁的內(nèi)存分配加上垃圾回收機(jī)制是否會(huì)導(dǎo)致—— C#, D, 和Java這些包含垃圾回收的語(yǔ)言嚴(yán)重的非線形,這明顯沒(méi)有。這個(gè)結(jié)果可以看出所有的語(yǔ)言/庫(kù)表現(xiàn)的非常合理的。這里我們可得出幾個(gè)有趣的結(jié)論:
1.C語(yǔ)言和C++語(yǔ)言,提供正確的庫(kù)支持,他們內(nèi)存分配空間最快。毫無(wú)疑問(wèn),部分原因由于它們沒(méi)有初始化字符數(shù)組的內(nèi)容。根據(jù)結(jié)果,我重新對(duì)C語(yǔ)言進(jìn)行測(cè)試,用calloc()替代malloc(),測(cè)試結(jié)果很接近C#,雖然仍然會(huì)高出5個(gè)百分點(diǎn)。
2.內(nèi)存碎片(只要輪流存取第三和第四個(gè)變量)不會(huì)在很大程度上影響內(nèi)存分配的性能,不會(huì)增加總體負(fù)擔(dān)。
3.如果垃圾回收器安排在沒(méi)有使用的內(nèi)存區(qū)之后不執(zhí)行(我假設(shè)這是可以的),這將不會(huì)對(duì)性能有太大的影響。我假設(shè),這說(shuō)明了C#是第一個(gè)會(huì)內(nèi)存枯竭的語(yǔ)言,所以我們可以假定它們通過(guò)使用大量的內(nèi)存空間在內(nèi)存分配性能方便取得平衡,并且相信在現(xiàn)實(shí)的環(huán)境中有這種機(jī)會(huì)運(yùn)行垃圾回收。
4.通常更希望體面地降低性能―就象C(Digital Mars)的方式――而不是在各個(gè)方面都有很強(qiáng)大的性能,然后在某些未知的閥值化為烏有:在服務(wù)器環(huán)境下,某一時(shí)刻提供一個(gè)慢點(diǎn)的服務(wù)比導(dǎo)致崩潰可能要好。由于對(duì)Digital Mars和Visual C++,C和C++的運(yùn)行實(shí)際上是相同的,我們可以假定因?yàn)榕c通過(guò)new操作符的調(diào)用的交流而增加的成本可以忽略,并且在C和C++之間沒(méi)有本質(zhì)的區(qū)別。
5.C#內(nèi)存分配時(shí)間會(huì)比Java快上3~4倍。
總的來(lái)說(shuō),這個(gè)結(jié)果并不象我想象的那樣:我期望C和C++在中小應(yīng)用比 C#,D,和Java稍微落后,但在大型應(yīng)用中遠(yuǎn)遠(yuǎn)優(yōu)于。真是學(xué)無(wú)止境。
Rafile . 對(duì)于文件的隨機(jī)存儲(chǔ)結(jié)果可以看出一些重點(diǎn)。我認(rèn)為最重要的一點(diǎn)是仔細(xì)的選擇庫(kù)。之所以C++的運(yùn)行效果看上去要比C和其他的語(yǔ)言好很多,就是因?yàn)閹?kù)的原因。當(dāng)把C++的性能(Intel 和VC6庫(kù))和其他語(yǔ)言/編譯器進(jìn)行比較時(shí),用其他任何一種都很給人留下深刻的印象。自然,這邊的例子是故意的,但這么大的性能差別的事實(shí)——比C#和 Java快23倍,我們可以期待在現(xiàn)實(shí)情況中性能的有意義的差別。(這里,再次證明了偏見是錯(cuò)誤的:我很厭惡所有的io流,從很多方面很容易能看出它的效率很低,當(dāng)然也不能說(shuō)全部這樣。我對(duì)于這樣的性能印象很深刻)。
詳細(xì)的總結(jié)了C#的性能以后,我想就我們?cè)谄渌Z(yǔ)言方面的研究發(fā)現(xiàn)做一個(gè)簡(jiǎn)短的介紹。
1.C處理異常事件是不錯(cuò)的,特別是在存儲(chǔ)和文件(提供正確的庫(kù)連接)有非常好的表現(xiàn);它能提供所有我們能預(yù)期的效果,但是與后來(lái)的高級(jí)語(yǔ)言相比,吸引不了更多的學(xué)習(xí)者。
2.C++處理異常事件也不錯(cuò),特別是在存儲(chǔ)和文件(提供正確的庫(kù)連接)有非常好的表現(xiàn),而且包含模塊的概念和有效的RAII;它始終得到我的鐘愛,我認(rèn)為它在未來(lái)的很長(zhǎng)一段時(shí)間內(nèi)都是編程的最佳選擇。
3.D在異常事件處理上不是很太好,在文件處理上處于一般水平,在存儲(chǔ)方面有比較合理的相關(guān)性能和非常不錯(cuò)的線性度,有模塊和RAII(雖然語(yǔ)言本身有很多的bug產(chǎn)生,使得很多的處理過(guò)程被掛起);當(dāng)這語(yǔ)言經(jīng)過(guò)它的初級(jí)階段,期望會(huì)得到不錯(cuò)的效果。
4.Java在封裝和內(nèi)存管理的性能上表現(xiàn)最差,不過(guò)它有好的異常事件處理,但沒(méi)有一點(diǎn)模塊和RAII的概念;它不指望會(huì)達(dá)到真正的美好,我認(rèn)為C#/.NET可以衡量出它的高低,至少運(yùn)行在Windows平臺(tái)上 。
摘要
從第一部分開始,就給出全文的延伸的意思,從性能方面,如何能更好的用C#/.NET寫出很好的軟件。在第一部分里,一些結(jié)果雖然不是很值得注意,但是有很多的地方還是令人叫奇的(至少我是這樣的?。?。
這些研究結(jié)果展示了C#可以提供非常好的性能效果——至少在我們測(cè)試的范圍內(nèi)是這樣——同時(shí)不能把它如同象過(guò)去把Visual Basic和一些擴(kuò)展版本的Java當(dāng)作一種性能差的語(yǔ)言來(lái)對(duì)待。對(duì)我而言,我比較注意語(yǔ)言運(yùn)行的性能,當(dāng)然,他們的表現(xiàn)也會(huì)超出我的預(yù)期效果。我所提出的嚴(yán)肅評(píng)論是從語(yǔ)言特征的觀點(diǎn)和對(duì)多個(gè)編程范例的支持方面得出的。
作為一個(gè)最初使用C++的程序員,我從特定的觀點(diǎn)得出這些比較結(jié)果。這可以從RAII和封裝測(cè)試以及它們的語(yǔ)言解釋上看的出來(lái)。在某種意義上來(lái)說(shuō),這就好比比較蘋果和橙子,對(duì)于不同背景的人們可能會(huì)對(duì)我為什么如此強(qiáng)調(diào)模板和確定性析構(gòu)感到疑惑,沒(méi)有這些他們也進(jìn)行的很好。
毫無(wú)疑問(wèn),模板給C++帶來(lái)了非凡的革命,它能支持群組合作,或者獨(dú)立運(yùn)行,范例是無(wú)可比擬的。Java由于缺少模板和強(qiáng)制要求任何東西都是對(duì)象,而被批評(píng)了很長(zhǎng)時(shí)間。.NET框架在這方面做法一樣也很讓人失望;可能由于Java缺少模板而他們可以在.NET環(huán)境下得到(他們確實(shí)達(dá)到)讓更廣的開發(fā)團(tuán)體感到信賴并接受了它。
缺少對(duì)RAII的支持對(duì)GUIs(通用用戶接口)是非常好的,這是在嚴(yán)格的框架內(nèi)操作的軟件——即使象J2EE這樣精密復(fù)雜和高吞吐量的語(yǔ)言,和非關(guān)鍵的程序。但復(fù)雜的軟件不必要因?yàn)樗鴱?fù)雜。在最好的情況下,到處都是最終模塊,只要在Finalize()里函數(shù)里添加Close()方法就可以了。在最壞的情況下,滯緩或者隨機(jī)的資源泄漏都會(huì)導(dǎo)致系統(tǒng)的崩潰。更甚于以上的,如果OO(面向?qū)ο螅拖驝#和Java所有都是對(duì)象——是你的目的那更讓我不安,第一個(gè)要失去的就是對(duì)象自己清除自己的責(zé)任,我沒(méi)辦法理解這個(gè)。(我知道一個(gè)非常出名的C++程序員——當(dāng)然我不能告訴是誰(shuí),他告訴我當(dāng)他在課程中突出RAII和模板的重點(diǎn)時(shí),那些非使用C++的人們露出的滑稽表情,讓他感覺好象他丟失了什么東西)
D是使用垃圾回收的語(yǔ)言,默認(rèn)是非確定性析構(gòu),有很多地方與C#和Java相似,但是盡管如此,它還是兼容支持RAII的習(xí)慣同時(shí)具有模板。并且它是一個(gè)人寫的!我不明白為什么我們對(duì)Java或者C#(.NET其它語(yǔ)言也是一樣)印象如此深刻,即使有它們支持RAII和模板的缺點(diǎn),而我們又能比較這些。有可能C#/.NET成功的原因和Java一樣,有大量的,有用的庫(kù)文件(C和C++應(yīng)該從中學(xué)習(xí),D應(yīng)該去發(fā)展),和有一個(gè)強(qiáng)有力的后臺(tái)。
最后,我想說(shuō)對(duì)于所有的性能結(jié)果的比較分析,你必須明智地使用這些結(jié)果。我努力使自己公平,選擇我相信是公平和有意義的測(cè)試。但你一定要判斷的出這些例子僅代表了廣大可能性的的一小部分(不是無(wú)限意義上的),它們不是實(shí)際的程序,僅僅是測(cè)試而已并且把它簡(jiǎn)化了,其中不可避免的帶有我個(gè)人的某種偏見在里面。通過(guò)逐步的方法,我希望降低這些因素。但你在閱讀和使用這些結(jié)果的時(shí)候要保持留意。
感謝
我要感謝 Walter Bright提供給我最新的Dv0.62版本,能夠完全的測(cè)試異常事件。感謝Intel的David Hackson給我提供了C/C++編譯器。還要感謝Scott Patterson幫我選擇了切合實(shí)際的測(cè)試方法(他總是不斷的在我煩躁、偏題、愚蠢的時(shí)候提醒我)。還要感謝Eugene Gershnik 對(duì)于我的一些斷言給了我嚴(yán)厲的反駁,幫助我適當(dāng)?shù)刈⒁庖恍┓乐拐`解的說(shuō)明。
Notes and References
[1] The C++ Programming Language, Bjarne Stroustrup, Addison Wesley, 1997
[2] The Software Optimization Cookbook, Richard Gerber, Intel Press, 2002
[3] Applied Microsoft .NET Framework Programming 1 st Edition, Jeffrey Richter, Microsoft Press, 2002
[4] Described in “ Win32 Performance Measurement Options, ” Matthew Wilson, Windows Developer Network, Volume 2 Number 5, May 2003.
在本書的第一部分,我講述了C#語(yǔ)言的許多基本方面——包括整型、字符型和浮點(diǎn)型之間的轉(zhuǎn)換,循環(huán)、迭代算法,以及字符串比較、拼接和 特定符號(hào)統(tǒng)計(jì) 。正如我所說(shuō),對(duì)一種新的語(yǔ)言與它的類似語(yǔ)言進(jìn)行全面、公平的性能比較,范圍將會(huì)很龐大,所以本書沿用第一個(gè)例子并只對(duì)有限的重要的部分特性做測(cè)試。這種比較,以先前的 C#及它的庫(kù)與C、C++、D和Java的特性對(duì)比為基礎(chǔ),主要在以下幾個(gè)領(lǐng)域:·類機(jī)制(封裝與模板)
·異常
·內(nèi)存— 重點(diǎn)
·大型文件的隨機(jī)訪問(wèn)
·資源獲取初始化
正如我在第一部分提到的,這里所做的測(cè)試只是針對(duì)程序性能的一小部分,硬件及操作系統(tǒng)的配置是測(cè)試語(yǔ)言 /編譯器/庫(kù)函數(shù)的機(jī)器的典型配置:2-GHZ,512-MB,Pentium IV,Windows XP Professional。 這里我們看到的任何結(jié)果揭示了在其它環(huán)境下可能的性能,這不說(shuō)明可以假設(shè)這些結(jié)果就是正確的典型例子。 最后要說(shuō)明的是,當(dāng)我提到“C#比Java好”等結(jié)論的時(shí)候,一定是指“在特定的測(cè)試環(huán)境的條件下”,這一點(diǎn)請(qǐng)你銘記在心。
性能評(píng)價(jià)
所有的代碼還是用和第一部分一樣的編譯器編譯。 C#代碼在.NET框架 1.0.3705(可從http://microsoft.com/netframework/免費(fèi)下載)下,由Visual C#.NET 編譯器v7.00.9466編譯。D代碼由Digital Mars D 編譯器 Alpha v0.62 (可從http://www.digitalmars.com/免費(fèi)下載) 編譯。Java代碼由J2DKSE1.4.1-02 ( 可免費(fèi)下載 http://java.sun.com/ ) 編譯。 C和C++代碼 ( 單線程 , 包括 raii , 和靜態(tài)鏈接庫(kù) ) 由使用 STLPort v4.5的Digital Mars C/C++ v8.33 ( 可從 http://www.digitalmars.com/免費(fèi)下載 ) 和使用 Visual C++ 6.0的頭文件和庫(kù)文件的Intel C/C++ v7.0來(lái)編譯。所有這些編譯器均使用最優(yōu)化處理 ( set for speed ) ; Intel 用-QaxW flag [2]提供兩套編碼路徑:一個(gè)是針對(duì)有SSE2的Pentium IV處理器,另一個(gè)是針對(duì)其他所有的處理器——在運(yùn)行時(shí)選擇合適的一個(gè)。(因?yàn)镮ntel編譯器只適用于Intel處理器構(gòu)造,特別是在這個(gè)測(cè)試中的Pentium IV,這編譯器的編譯結(jié)果必須在此基礎(chǔ)上評(píng)價(jià),不能在其他的處理器上進(jìn)行同樣的過(guò)程,比如Athlon,甚至是舊版本的Intel處理器也不行。)
后面將要描述我們測(cè)試的假想的五種例子中的性能表現(xiàn)。他們都用2-GHZ,512-MB,Pentium IV,Windows XP Professional的配置,并沿用在第一部分用的性能分析系統(tǒng)。每一種語(yǔ)言所編程序的調(diào)試都執(zhí)行7次,同時(shí)沒(méi)有其他占線過(guò)程,去掉最高速的和最低速的,紀(jì)錄平均值。所有的時(shí)間信息,通過(guò)僅區(qū)分相應(yīng)時(shí)間區(qū)間的高性能計(jì)時(shí)器保存下來(lái)。(C用戶調(diào)用Win32 API QueryPerformanceCounter() ;C++ 用 Win-STL ' s performance_counter [4], 可在http://winstl.org/找到 ; C# 用 SynSoft.Performance.PerformanceCounter ,可在 http://synsoft.org/dotnet.html找到; D 用 synsoft.win32.perf.PerformanceCounter , 可在http://synsoft.org/d.html找到;Java 用 System.currentTimeMillis() .)每一個(gè)測(cè)試程序都包含一個(gè)啟動(dòng)迭代的程序,這樣可以將啟動(dòng)或緩沖時(shí)的影響最小化。
在每一個(gè)例子中的C#,C++和D的特征: C在所有 保存 封裝和 raii( 類)的地方出現(xiàn),因?yàn)樗鼪](méi)有這兩種例子必需的語(yǔ)言特征。 raii 沒(méi)有出現(xiàn)Java,因?yàn)镴ava甚至連 Resource AcquisitionIs Initialization (RAII [1])的機(jī)制也沒(méi)有,所以它僅僅作為在GC-kicking的一個(gè)練習(xí)。簡(jiǎn)單的說(shuō),我不會(huì)展示所有的語(yǔ)言的每一個(gè)例子的所有特性,但是所有這些都包含于在線記錄中(連同產(chǎn)生他們的Digital Mars make-compatible make-file)。
下面就對(duì)測(cè)試性能的例子進(jìn)行評(píng)價(jià) :
封裝 。這個(gè)例子( Listing1)的目的是比較不同語(yǔ)言(除了C)在類機(jī)制上的代價(jià),這些語(yǔ)言并不完全支持相對(duì)于它們所做的所有類型的一般處理。尤其是C#和Java一般不能存儲(chǔ)和使用它們的內(nèi)置類型實(shí)例,比如int,long,bool等等,同樣,對(duì)于從這些實(shí)例派生的類型也不行。相反C++和D都支持模板,它可以給任何類型的一般性操作帶來(lái)方便。在以后的文章里,我將詳細(xì)地講解容器。但在這個(gè)章節(jié)將不會(huì)用到它,如果提及它反而會(huì)分散對(duì)于封裝意義的理解。C#和Java中未封裝的變量傳遞ints給異或函數(shù),封裝變量傳遞整形值在Int32(C#)和Interger(Java)的實(shí)例中。注意Java不支持模糊的封裝(這被證明是好的因?yàn)榫幾g器將對(duì)于這種封裝帶來(lái)的低效給予警告),所以實(shí)例必須明確地被創(chuàng)建(在列表1中標(biāo)記“Java ” 處可以看到)。 C++和D的運(yùn)行是簡(jiǎn)單的使用模板,這就是它們的泛型機(jī)制。(在這里我不對(duì)“一切都是對(duì)象”的模板的好處作深入探討,但是我將在這期的姊妹篇Stepanov camp中對(duì)它做詳細(xì)講解)。
異常處理 。異常的語(yǔ)義在各種語(yǔ)言之間有很大的區(qū)別。 在 C++,異常處理和調(diào)用析構(gòu)函數(shù)一起沿著堆棧操作下來(lái),然而在有垃圾回收的語(yǔ)言中,這些對(duì)象被丟棄,直到將來(lái)的某一時(shí)刻被收集。 這樣,就無(wú)法對(duì)各種語(yǔ)言進(jìn)行有意義的比較。所以在這個(gè)例子中,只能通過(guò)將一段 using/aware代碼與另一段沒(méi)有異常處理支持的相同代碼進(jìn)行比較。這個(gè)例子有三種測(cè)試,它們的目的就是去衡量使用和拋出異常處理的代價(jià)。第一(Listing 2)是比較使用異常處理和通過(guò)函數(shù)返回值來(lái)傳遞信息給調(diào)用者這兩者的代價(jià)。第二(Listing 3)是評(píng)估在try-catch模塊中執(zhí)行的代價(jià)(盡管沒(méi)有異常處理被拋出)。最后(Listing 4)就是評(píng)估遍歷執(zhí)行try-catch模塊的代價(jià)(同樣,這沒(méi)有異常處理被拋出)。
Mstress。 這一個(gè)例子的目的是,在假設(shè)分配比回收優(yōu)越的基礎(chǔ)上,看看垃圾回收語(yǔ)言是否在高的內(nèi)存利用率上有重要的非線形關(guān)系。這里存在四種測(cè)試,每一個(gè)都牽涉到分配與釋放( C 和 C++)或者丟棄(C#, D, 和 Java)在一個(gè)時(shí)間段內(nèi)的內(nèi)存塊。這些塊大小是通過(guò)相同的偽隨機(jī)數(shù)算法生成的(詳見文章的第一部分),這能使塊的大小處于1至1000之間,從而在確保每種語(yǔ)言在相同的環(huán)境下測(cè)試時(shí)仿效現(xiàn)實(shí)世界中的分配。第一種測(cè)試(Listing 5)通過(guò)一個(gè)不斷循環(huán)的變量分配了1000個(gè)內(nèi)存塊,目的是看看頻繁地分配會(huì)不會(huì)“留下”更多的碎片,這些碎片會(huì)阻礙分配的進(jìn)行直到突然產(chǎn)生明顯的GC收集。第二種測(cè)試(Listing 5)增加了1000次循環(huán)中分配的次數(shù),以測(cè)試由不斷增加的內(nèi)存負(fù)擔(dān)引起的性能的 損失 。 第三和第四種測(cè)試(沒(méi)列出來(lái))跟前兩個(gè)相似,它們?cè)黾恿肆硗獾膬?nèi)存塊和分配/回收的交叉活動(dòng),目的是為了模擬不斷增加的內(nèi)存碎片。 C使用malloc()/free(),而C++,C#,D和JAVA用new[]。
rafile 。這一個(gè)例子( Listing 6)評(píng)估各種語(yǔ)言從大文件中隨機(jī)讀取的能力。它利用某種偽隨機(jī)算法(詳見文章第一部分)來(lái)模擬在大文件中的查找/讀取。這里用到了兩個(gè)文件,一個(gè)4.2MB,一個(gè)21.1MB。在每一個(gè)查找點(diǎn),四個(gè)字節(jié)被讀取,并且和一個(gè)運(yùn)行值進(jìn)行異或運(yùn)算(這樣有利于證明所有的語(yǔ)言以相同的方式查找和讀?。?。有意思的是,原始的運(yùn)行程序讀了32位的整數(shù),但不幸的是,JAVA用了network byte-order而不是host byte-order,這與C, C++, 和D的machine-order reading是不相容的。
Raii(類) : 這最后的例子目的是為了評(píng)估沒(méi)有內(nèi)建支持這個(gè)非常重要的語(yǔ)言特征(很不幸 C#和Java都沒(méi)有)而付出的代價(jià)。此例子表達(dá)了通過(guò)資源句柄XUser 類型從XFactory資源管理器分配X資源到客戶端代碼(見代碼段7)。在C++和D語(yǔ)言中,XUser 提供一個(gè)析構(gòu)函數(shù),在析構(gòu)函數(shù)里分配的資源自動(dòng)地回收到資源庫(kù),資源在這種情況下是個(gè)信號(hào)量,.NET和D的執(zhí)行代碼可從 http://synsoft.org/dotnet.html 和 http://synsoft.org/d.html 下載。C#、D和Java 提供它們自己抽象的同步機(jī)制,但我需要一個(gè)信號(hào)量去引起交叉線程的競(jìng)爭(zhēng),去保證同樣的同步機(jī)制以便所有語(yǔ)言的進(jìn)行生動(dòng)的比較。此信號(hào)量在每種語(yǔ)言執(zhí)行代碼中都有最初可獲得的五個(gè)鍵值。
盡管 C#的XUser 類有一個(gè)析構(gòu)函數(shù),但此析構(gòu)函數(shù)僅在實(shí)例進(jìn)行垃圾碎片收集時(shí)被調(diào)用。(C#使用析構(gòu)函數(shù)的語(yǔ)法,但編譯器把它翻譯成 CLR (公共語(yǔ)言運(yùn)行時(shí)刻)的 Finalize() 方法。)這種情況跟 Java一樣。C# 還盡量更多地支持習(xí)慣用語(yǔ)(雖然它支持的還不夠),這是通過(guò)使用引用聲明來(lái)實(shí)現(xiàn)的,正如表7所示。(為了方便,“using-ed”類型必須實(shí)現(xiàn) IDisposable 接口,否則聲明無(wú)效),一個(gè)明顯的問(wèn)題是類的使用者必須承擔(dān)確保語(yǔ)義的正確的負(fù)擔(dān),而不是類的開發(fā)者。這是奇怪的二分法:C#是面向?qū)ο蟮模揽款惖氖褂谜邔?duì)類的內(nèi)部執(zhí)行代碼的足夠理解以決定是否需要此類的直接析構(gòu)。C++和D語(yǔ)言過(guò)程明確,由類自己決定:C++通過(guò)析構(gòu)函數(shù)的執(zhí)行,D提供析構(gòu)或終止函數(shù),類聲明驗(yàn)證關(guān)鍵字 AUTO (沒(méi)有這些,它按C#和Java的方式運(yùn)行正常的垃圾碎片收集程序)。
C#語(yǔ)言使用 USING 聲明(和 IDisposable 接口)的一個(gè)可替代方法是依靠垃圾回收。在形成這個(gè)情況之前,我假設(shè)當(dāng)內(nèi)存使用率達(dá)到觸發(fā)閥值或者主線程空閑/等待的時(shí)候啟動(dòng)垃圾碎片回收。(我不知道這種誤解是怎么形成的,或許只是異想天開)。當(dāng)我在執(zhí)行 Raii2 例子的最初版本的時(shí)候(變量沒(méi)有使用 USING 聲明),處理進(jìn)程馬上掛起。很明顯,在其他線程堵塞時(shí)垃圾碎片收集沒(méi)有觸動(dòng),只有在內(nèi)存消耗到一定程度才觸動(dòng)。因此,為了程序執(zhí)行,我們被迫在另一個(gè)工作線程中自己剔除垃圾碎片收集。當(dāng)然,這在實(shí)際編程中是非??尚Φ膱?zhí)行策略,執(zhí)行結(jié)果證明了這一點(diǎn)。
結(jié)論
大部分的結(jié)果是以絕對(duì)運(yùn)行時(shí)間數(shù)(全部循環(huán)或每循環(huán))或者 C#的執(zhí)行時(shí)間和C,C++,D,Java分別的百分比來(lái)表達(dá)的。在后一種方式,圍繞著百分刻度的結(jié)果表示C#的性能比率,高的值表示C#的性能優(yōu)越,低的值表示相對(duì)低下。 Raii 的情況是個(gè)例外,在這里結(jié)果是以C++(Digital Mars)時(shí)間的百分比表示的,由于垃圾碎片收集代碼的消耗,調(diào)用時(shí)間值是以對(duì)數(shù)刻度表示的。這說(shuō)明在對(duì)數(shù)比例上歸根于非常高的垃圾處理花費(fèi).
![]() |
BOX . 從圖 1中我們能看出,在合理范圍里(C#語(yǔ)言的97%-271%)對(duì)于所有編譯語(yǔ)言解除封裝操作(就簡(jiǎn)單整形數(shù)據(jù))成本的差別:最差的是C++(Digital Mars),成本是C#的271%,最好的是C++(Intel) 成本是C#的97%.假設(shè)執(zhí)行相對(duì)比較簡(jiǎn)單的 異或 運(yùn)算, C#幾乎可以像 Intel那樣出色,你對(duì)這個(gè)并不會(huì)感到很驚奇,但你相信C#要比C++(Digital Mars)和D模板實(shí)例快兩倍.我有些驚訝C++(Digital Mars)這個(gè)相對(duì)弱的性能,特別是和D比起來(lái)它要慢得多,然而他們出自同一開發(fā)商。
顯然這個(gè)封裝花費(fèi)是有意義的 ,相當(dāng)于C++模板花費(fèi)4-10倍.這個(gè)測(cè)試僅僅包括整數(shù),相對(duì)來(lái)說(shuō)可能不能完全反映特殊類型.雖然如此,我們能有把握斷言這個(gè)模板輕易贏得了性能戰(zhàn).有趣的是就他們各自的成熟度而言,C#要比java稍好,
差距并不大 (小于4%).(我想可能由于.NET處理目標(biāo)JIT3?!?】)。更有趣的是,事實(shí)上在未封裝的形態(tài)下,c#明顯比java快.
除了圖 1以外,有人認(rèn)為,不拋出的異常variants都是是比拋出的異常副本要快的(如圖2). 用 c和c++,Intel完成任務(wù)要比所有其它不拋出的異常variants快.c#緊隨其后。似乎c,c++,D(不考慮編譯器)在異常處理上花費(fèi)大致相等,符合win32異常處理機(jī)制的預(yù)期限制因素.我們能看到三種語(yǔ)言在拋出異常variants上的差別是比較小的,很可能的原因是由于有相關(guān)的額外的管理消耗關(guān)系 (c++規(guī)定執(zhí)行破壞功能的堆棧是從捕獲點(diǎn)到拋出點(diǎn)的來(lái)回).
我發(fā)現(xiàn)有趣的是在拋出異常variants方面c#和java的執(zhí)行是有關(guān)系的.
因?yàn)檫@兩種語(yǔ)言語(yǔ)義在異常拋出處理要?jiǎng)儆诜祷禺?dāng)前錯(cuò)誤值 ,也勝于其它語(yǔ)言,
尤其 c語(yǔ)言所呈現(xiàn)的關(guān)系執(zhí)行調(diào)用.我非常贊同Brian Kernighan和Rob Pike
(<>,Addison Wesley,1999)所說(shuō)的例外僅僅在例外條件的使用,并不是必然會(huì)發(fā)生的.然而人們能容易看到為何服務(wù)器在處理巨大數(shù)量的數(shù)據(jù)的時(shí)候可能出現(xiàn)的例外,而經(jīng)常擔(dān)心性能的問(wèn)題.正是如此,java以小于其它語(yǔ)言25%的消耗給人深刻的印象.由此我們限定win32,采用兩個(gè)有代表性的差不多的c編譯器來(lái)構(gòu)造例外處理,雖然沒(méi)有定論,但是我認(rèn)為java使用了不同的機(jī)制.
那么c#的異常拋出性能如何呢?在相關(guān)條件下,相對(duì)其他語(yǔ)言而言,使用異常處理語(yǔ)句是不使用異常處理語(yǔ)句的21-77倍,而c#則是191倍;鑒于Java的出色表現(xiàn),.net工具應(yīng)該引起注意了。
Except-2. 這個(gè)圖表是為了弄清楚在一個(gè) try-catch范圍內(nèi)異常處理語(yǔ)句的執(zhí)行是否耗時(shí),如圖3,大多數(shù)語(yǔ)言的表現(xiàn)同預(yù)期的一樣:c、c++和D對(duì)于Digital Mars是類似的;Intel c 和c++比其它都好.c#要比java快.相比較就性能而言,
c#是很優(yōu)勢(shì)的,令人感興趣的是,c#在進(jìn)行異常處理是耗時(shí)比不進(jìn)行時(shí)還要少.產(chǎn)生這種結(jié)果肯定是有原因的(這個(gè)結(jié)果是經(jīng)過(guò)了多次驗(yàn)證的), 但由于對(duì)于c#異常處理機(jī)制我沒(méi)有很深的認(rèn)識(shí),我不能做出合理的解釋.同樣,雖然并沒(méi)有定論,對(duì)于D來(lái)說(shuō),是否進(jìn)行異常處理對(duì)結(jié)果似乎并無(wú)影響!.
我認(rèn)為這是因?yàn)樗挥们宄龡?nèi)數(shù)據(jù), java也有大致相同的自由度,并且表現(xiàn)出的差異也并不大。
except-3 如圖表 4所示,測(cè)試結(jié)果與預(yù)期大體相符。除了D編譯器的結(jié)果中微不足道的差異,所有的語(yǔ)言在遍歷異常處理語(yǔ)句的時(shí)候都比不進(jìn)行是耗時(shí)要長(zhǎng),盡管如此,我仍然對(duì)在這兩種情況下差異如此之小感到吃驚。我們通常認(rèn)為使用異常處理機(jī)制會(huì)耗費(fèi)時(shí)間,并且我認(rèn)為遍歷異常處理語(yǔ)句會(huì)在啟動(dòng)和完成上耗費(fèi)大量時(shí)間;事實(shí)上,我一開始就預(yù)期兩者的差異將會(huì)非常地顯著。然而,在這個(gè)測(cè)試當(dāng)中,兩者的差異相當(dāng)不明顯。
但是這個(gè)結(jié)果并不難理解,導(dǎo)致差異如此小的原因首先在于:這次測(cè)試太過(guò)于純粹。這次測(cè)試的所有異常處理(類型)都是 int型的。盡管并沒(méi)有異常被拋出,這樣做的目的在于避免在異常處理體的構(gòu)造(和析構(gòu))上耗費(fèi)時(shí)間和消除各編譯器處理機(jī)制不同所造成的影響。其次,本次測(cè)試中沒(méi)有基于框架的結(jié)構(gòu)體,因?yàn)樵贑++中,盡管析構(gòu)函數(shù)并不會(huì)真正被調(diào)用,析構(gòu)函數(shù)調(diào)用方面的準(zhǔn)備也是必須進(jìn)行的,這樣也會(huì)造成時(shí)間的耗費(fèi)。(注意析構(gòu)函數(shù)無(wú)論異常是否被拋出都會(huì)被調(diào)用。)這些耗費(fèi)都會(huì)造成測(cè)試的不公平,所以我們選擇了int型。注意到程序不論是因?yàn)楫惓伋龆兄惯€是正常退出,析構(gòu)函數(shù)都是要被調(diào)用的。所以它僅僅做為勾子而被添加進(jìn)這些額外的函數(shù)調(diào)用中。然而,我認(rèn)為所有的這些因素并不是很充足,它們僅僅使我們可以從那張表里知道當(dāng)不使用的時(shí)候異常的開銷是非常小的。很自然,這也正確的表明了一個(gè)人對(duì)所有語(yǔ)言性能(perform)的期望,當(dāng)然我們要給它足夠的信任.
metress-1 這個(gè)測(cè)試很清楚的表明了在不使用內(nèi)存分配機(jī)制的展示 (exhibit)中不斷增長(zhǎng)的對(duì)固定數(shù)量的內(nèi)存快(隨機(jī)大?。┓峙?釋放的循環(huán)的反應(yīng)是非線性的。事實(shí)上,在很大程度上它們從沒(méi)有線性(增長(zhǎng))的趨勢(shì):從性能上來(lái)說(shuō)并沒(méi)有什么變化(見圖5),除了在一些低循環(huán)(low interations)上有所不同之外.(有趣的是,那些低循環(huán)的非線性是有意義的--占到了全部命令的50%還多--當(dāng)然這僅僅是對(duì)c#和java而言).不管內(nèi)存分配機(jī)制是否在每次特循環(huán)結(jié)束后都立即恢復(fù)對(duì)1000個(gè)內(nèi)存塊的釋放,或者僅是簡(jiǎn)單的把它們交給GC在以后的某個(gè)時(shí)間處理,語(yǔ)言/運(yùn)行庫(kù)總是看上去幾乎完全不受這些循環(huán)執(zhí)行的影響.
在這組性能相對(duì)的測(cè)試中,我們可以清楚的看到一些它們之間的不同。在使用 Digital Mars 分配方式的語(yǔ)言中,C和C++的表現(xiàn)是最好的。Visual C++的運(yùn)行庫(kù)比使用Digital Mars的語(yǔ)言低大概2.5-3倍,對(duì)于使用Digital Mars 的語(yǔ)言和Visaul C++運(yùn)行庫(kù)來(lái)說(shuō),C和C++基本上是相同的。最后,很明顯,Java 比C#慢了3-4倍,而比C慢了差不多7倍.
mestress -2
就像我們從圖表中看到的,在一定數(shù)目的分配釋放內(nèi)存的循環(huán)中內(nèi)存塊(隨機(jī)大小)的不斷增長(zhǎng)的反應(yīng)是變量的引用是非線性表現(xiàn)的。這正是我們所希望的,因?yàn)樵诿看窝h(huán)中分配的內(nèi)存總量是按指數(shù)方式增長(zhǎng).每次循環(huán)所分配的內(nèi)存的大小都低于10,000塊.使用Digital Mars分配方式的C 和C++的效率依然是極為優(yōu)秀的。只是效率表現(xiàn)上低于平均值,而這里也不得不提到j(luò)ava,它的表現(xiàn)同樣不好.Visual C++的運(yùn)行庫(kù)(C和C++都適用)相對(duì)于C#,D 和Java來(lái)說(shuō),有一個(gè)不錯(cuò)的開始,但它很快就降到了一個(gè)很低的水平上。
Java在每次循環(huán)是使用10,000內(nèi)存塊之前的表現(xiàn)非常有競(jìng)爭(zhēng)力,并在這一點(diǎn)急劇上升。D在整個(gè)測(cè)試中都幾乎有著一致表現(xiàn).幾乎一直都是非線性的,這與C#很接近。
如果可能的話,我都希望看到一條總夠長(zhǎng)的X軸,C#在內(nèi)存溢出的情況下仍然保持每次循環(huán)不超出10,000內(nèi)存塊的水平并預(yù)防這種情況的出現(xiàn)。(D和Java似乎也做的到,但那也只是類似C#的行為一旦被發(fā)現(xiàn)就中止測(cè)試。)
mstress-3 和 4 從前兩個(gè)測(cè)試(variants)看,除了時(shí)間的的增長(zhǎng),反復(fù)的交叉存取和塊變量在執(zhí)行上的表現(xiàn)并沒(méi)有什么不同。曲線的走勢(shì)或者說(shuō)不同的語(yǔ)言/運(yùn)行庫(kù)的相對(duì)表現(xiàn)在這一點(diǎn)上沒(méi)有什么明顯的改變。
我認(rèn)為C和C++,在使用外部free/delete的條件下,重新使用的新近釋放的碎片。相反的,我很難想像出C#,D和Java 是如何使用垃圾收集機(jī)制周期性尋找每次循環(huán)所分配的內(nèi)存從而盡可能的減少由內(nèi)存碎片所引起的負(fù)面效應(yīng)的,或者在這個(gè)跟本就沒(méi)有產(chǎn)生碎片.除去這些不同,這兩種方式的表現(xiàn)還是很相似的。
這只是一種理想的我們所希望的分配機(jī)制的表現(xiàn) ---畢竟,那是一個(gè)很極端情況,所有內(nèi)存分配都可以在給定的周期內(nèi)完全返回---雖然程序的執(zhí)行都達(dá)到的目的。
rafile. 這次測(cè)試中我所希望是那些語(yǔ)言實(shí)際 (效率)上并沒(méi)有什么不同.除了C++的執(zhí)行也許比其它的低上幾個(gè)百分點(diǎn).
圖表 7中可以看出,C#的在文件的隨機(jī)存取上比C(使用Digital Mars)要好,但低于C++(使用Intel和VC6的連接庫(kù)),和D與Java現(xiàn)表現(xiàn)基本持平。但是C++運(yùn)行庫(kù)表現(xiàn)出令印象深刻的性能。
從所有這一系列測(cè)試來(lái)看,Intel 似乎已經(jīng)能夠產(chǎn)生性能不錯(cuò)的代碼了。但是它的連接的運(yùn)行庫(kù)頭文件卻是Visual C++ 6.0的,這個(gè)(大概)不是由Intel編譯器產(chǎn)生的。因比它幾乎是以壓倒性的性能優(yōu)勢(shì)超過(guò)了DMC,這主要是由于各自的Digital Mars 和微軟Visual C++運(yùn)行庫(kù)的性能。(我承認(rèn)有點(diǎn)吃驚,對(duì)于些測(cè)試結(jié)果正好可以反對(duì)兩個(gè)賣家的名聲--應(yīng)該或是不應(yīng)該得到的)。這也表明一個(gè)人的偏見是非常沒(méi)有理由的。
另一件值的注意的事就是對(duì)不同大小文件訪問(wèn)的開銷都非常的小。這也表明所有的語(yǔ)言都利用操作系統(tǒng)的優(yōu)勢(shì)很輕易的達(dá)到了相同的程度。
raII . 從圖 8中我們能看出使用statement的C#的表現(xiàn)只有C++析構(gòu)函數(shù)效果的一半。D的性能與之相當(dāng),雖然產(chǎn)生的代碼中有錯(cuò)誤(或者是D的Phobos運(yùn)行庫(kù)),當(dāng)進(jìn)程創(chuàng)建的對(duì)像超過(guò)32,000左右的時(shí)候會(huì)使該進(jìn)程掛起,從而防止過(guò)多(一個(gè)以上的)通訊中的數(shù)據(jù)點(diǎn)被確定的使用.在這組單獨(dú)的測(cè)試中我們可以看到RAII對(duì)C#和D的支持是很完善的,但并不如想象中那樣優(yōu)秀。如有你要做很多scoping--并且你想,也許要足夠的robustaness的幫助---你最好還是選擇(stay with)C++。當(dāng)依賴于.NET的垃圾回收機(jī)制時(shí),因?yàn)樘幚磉^(guò)程只是簡(jiǎn)單地被掛起,所以C#的性能比較難以測(cè)算。可是我們不希望.NET經(jīng)常調(diào)用它的垃圾回收線程,但我找不到理由解釋為什么回收線程不以較低優(yōu)先級(jí)在內(nèi)核中等待,當(dāng)有垃圾對(duì)象需要被回收和主線程空閑時(shí)再進(jìn)行有效的回收。當(dāng)然,我在垃圾回收方面不是專家,并且可能有一種合理的理論原因說(shuō)明這為什么不是一個(gè)好的處理方法。
GC(垃圾回收)即使響應(yīng)的更靈敏,我們可以在它的性能結(jié)果上看出效果并不與之匹配。對(duì)應(yīng)第二組數(shù)據(jù)指針的方法表明了GC每隔1ms被觸發(fā)一次。
GC(垃圾回收)的方法比使用使用聲明慢了近千倍,所以這不僅僅是一個(gè)技術(shù)問(wèn)題的范圍。自然,這跟關(guān)于這個(gè)例子的假設(shè)有關(guān),同時(shí)跟幾乎每個(gè)人都用垃圾回收機(jī)制去管理頻繁爭(zhēng)用的資源的釋放這個(gè)不辯的事實(shí)有關(guān),但我沒(méi)預(yù)計(jì)到這個(gè)變量的性能是如此之差。同時(shí)這也給了讀過(guò)Java/.NET書籍的C++程序員對(duì)于書籍建議(或許說(shuō)依靠)使用終止函數(shù)來(lái)清除資源而感覺疑惑的一個(gè)解釋。
結(jié)論
在第一篇文章,我對(duì)于不同的環(huán)節(jié)得出不同的結(jié)論,這些環(huán)節(jié)或是語(yǔ)言本身造成的,或是庫(kù)或者二者造成的,在這里我也這么區(qū)分,因?yàn)檎Z(yǔ)言特征而產(chǎn)生的影響的部分通常涉及:異常,封裝 /模板和RAII的實(shí)現(xiàn)。文件存取部分可以看作直接對(duì)庫(kù)函數(shù)的操作。(沒(méi)有任何一種語(yǔ)言阻止寫替換操作,雖然這樣做并不是很容易)。內(nèi)存管理部分受語(yǔ)言和內(nèi)存的影響――雖然我不清楚可以用哪一種管理機(jī)制去替代C#和JAVA的缺省的內(nèi)存管理機(jī)制,但是對(duì)于其他語(yǔ)言這是很簡(jiǎn)單的。
封裝。當(dāng)我們比較封裝和模板時(shí),我們可以看出模板明顯比封裝出色很多。我相信這不會(huì)讓大家感到很驚奇,但是事實(shí)上封裝消耗的系統(tǒng)資源是模板的十倍。自然,作為模板類庫(kù)的作者( http://stlsoft.org/ ),我得出這個(gè)結(jié)論也許帶有偏見,但是這些數(shù)據(jù)在表示更廣泛的情況時(shí),他們自身就說(shuō)明這一點(diǎn)。在后面,我將說(shuō)到容器和算法,我們將看到比預(yù)計(jì)更多的這樣的結(jié)果。
至于有關(guān)異常情況,我想講四點(diǎn):
1.在所有的程序語(yǔ)言中,使用異常處理從調(diào)用的函數(shù)過(guò)程中返回,而不是保留他們作為異常事件的指示,將導(dǎo)致執(zhí)行花費(fèi)巨大的成本。
2.C#的異常處理機(jī)制相對(duì)于其他被測(cè)試的語(yǔ)言效率是極低的.
3.在有異常處理的上下文環(huán)境下運(yùn)行(比如用try-catch的情況)對(duì)于性能沒(méi)有多大影響,除了C#,它為了提高性能(實(shí)際上,我對(duì)于這種結(jié)果很不理解,并且懷疑這是.NET運(yùn)行期的一個(gè)人為結(jié)果而不是C#/.NET的一般特征。)
4.交叉于異常上下文的執(zhí)行(比如說(shuō)進(jìn)入try-catch和/或 離開try-catch)對(duì)于系統(tǒng)性能的影響基本上是非常小的。
Raii. C#支持的RAII例子比C++在性能方面差,雖然不是很多,但與D相比差別無(wú)幾,基本一致。(這種一致指出了在處理堆分配對(duì)象的確定性析構(gòu)時(shí)的基本限制)然而,從理論觀點(diǎn),或從易用性和/或穩(wěn)健性的實(shí)踐觀點(diǎn)來(lái)看,這里還是有很大差距的。C語(yǔ)言缺乏機(jī)制可以解釋為年代已久,并且它是一種程序語(yǔ)言。很遺憾,java缺乏這種機(jī)制,但是它只是可以解釋為忽視了。(至今為止我們已經(jīng)用java8年左右了,所以“忽視”可能也有些牽強(qiáng)。)C#在這方面的努力還不成熟(因?yàn)樗嗟匾蕾囶惖挠脩舳皇穷惖淖髡撸?,很奇怪的是C#/.NET很多優(yōu)點(diǎn)都是在Java中被看作瑕疵/遺漏的地方,比如屬性,輸出參數(shù),和無(wú)符型。
Mstress. 這個(gè)內(nèi)存測(cè)試的目的是證明如果頻繁的內(nèi)存分配加上垃圾回收機(jī)制是否會(huì)導(dǎo)致—— C#, D, 和Java這些包含垃圾回收的語(yǔ)言嚴(yán)重的非線形,這明顯沒(méi)有。這個(gè)結(jié)果可以看出所有的語(yǔ)言/庫(kù)表現(xiàn)的非常合理的。這里我們可得出幾個(gè)有趣的結(jié)論:
1.C語(yǔ)言和C++語(yǔ)言,提供正確的庫(kù)支持,他們內(nèi)存分配空間最快。毫無(wú)疑問(wèn),部分原因由于它們沒(méi)有初始化字符數(shù)組的內(nèi)容。根據(jù)結(jié)果,我重新對(duì)C語(yǔ)言進(jìn)行測(cè)試,用calloc()替代malloc(),測(cè)試結(jié)果很接近C#,雖然仍然會(huì)高出5個(gè)百分點(diǎn)。
2.內(nèi)存碎片(只要輪流存取第三和第四個(gè)變量)不會(huì)在很大程度上影響內(nèi)存分配的性能,不會(huì)增加總體負(fù)擔(dān)。
3.如果垃圾回收器安排在沒(méi)有使用的內(nèi)存區(qū)之后不執(zhí)行(我假設(shè)這是可以的),這將不會(huì)對(duì)性能有太大的影響。我假設(shè),這說(shuō)明了C#是第一個(gè)會(huì)內(nèi)存枯竭的語(yǔ)言,所以我們可以假定它們通過(guò)使用大量的內(nèi)存空間在內(nèi)存分配性能方便取得平衡,并且相信在現(xiàn)實(shí)的環(huán)境中有這種機(jī)會(huì)運(yùn)行垃圾回收。
4.通常更希望體面地降低性能―就象C(Digital Mars)的方式――而不是在各個(gè)方面都有很強(qiáng)大的性能,然后在某些未知的閥值化為烏有:在服務(wù)器環(huán)境下,某一時(shí)刻提供一個(gè)慢點(diǎn)的服務(wù)比導(dǎo)致崩潰可能要好。由于對(duì)Digital Mars和Visual C++,C和C++的運(yùn)行實(shí)際上是相同的,我們可以假定因?yàn)榕c通過(guò)new操作符的調(diào)用的交流而增加的成本可以忽略,并且在C和C++之間沒(méi)有本質(zhì)的區(qū)別。
5.C#內(nèi)存分配時(shí)間會(huì)比Java快上3~4倍。
總的來(lái)說(shuō),這個(gè)結(jié)果并不象我想象的那樣:我期望C和C++在中小應(yīng)用比 C#,D,和Java稍微落后,但在大型應(yīng)用中遠(yuǎn)遠(yuǎn)優(yōu)于。真是學(xué)無(wú)止境。
Rafile . 對(duì)于文件的隨機(jī)存儲(chǔ)結(jié)果可以看出一些重點(diǎn)。我認(rèn)為最重要的一點(diǎn)是仔細(xì)的選擇庫(kù)。之所以C++的運(yùn)行效果看上去要比C和其他的語(yǔ)言好很多,就是因?yàn)閹?kù)的原因。當(dāng)把C++的性能(Intel 和VC6庫(kù))和其他語(yǔ)言/編譯器進(jìn)行比較時(shí),用其他任何一種都很給人留下深刻的印象。自然,這邊的例子是故意的,但這么大的性能差別的事實(shí)——比C#和 Java快23倍,我們可以期待在現(xiàn)實(shí)情況中性能的有意義的差別。(這里,再次證明了偏見是錯(cuò)誤的:我很厭惡所有的io流,從很多方面很容易能看出它的效率很低,當(dāng)然也不能說(shuō)全部這樣。我對(duì)于這樣的性能印象很深刻)。
詳細(xì)的總結(jié)了C#的性能以后,我想就我們?cè)谄渌Z(yǔ)言方面的研究發(fā)現(xiàn)做一個(gè)簡(jiǎn)短的介紹。
1.C處理異常事件是不錯(cuò)的,特別是在存儲(chǔ)和文件(提供正確的庫(kù)連接)有非常好的表現(xiàn);它能提供所有我們能預(yù)期的效果,但是與后來(lái)的高級(jí)語(yǔ)言相比,吸引不了更多的學(xué)習(xí)者。
2.C++處理異常事件也不錯(cuò),特別是在存儲(chǔ)和文件(提供正確的庫(kù)連接)有非常好的表現(xiàn),而且包含模塊的概念和有效的RAII;它始終得到我的鐘愛,我認(rèn)為它在未來(lái)的很長(zhǎng)一段時(shí)間內(nèi)都是編程的最佳選擇。
3.D在異常事件處理上不是很太好,在文件處理上處于一般水平,在存儲(chǔ)方面有比較合理的相關(guān)性能和非常不錯(cuò)的線性度,有模塊和RAII(雖然語(yǔ)言本身有很多的bug產(chǎn)生,使得很多的處理過(guò)程被掛起);當(dāng)這語(yǔ)言經(jīng)過(guò)它的初級(jí)階段,期望會(huì)得到不錯(cuò)的效果。
4.Java在封裝和內(nèi)存管理的性能上表現(xiàn)最差,不過(guò)它有好的異常事件處理,但沒(méi)有一點(diǎn)模塊和RAII的概念;它不指望會(huì)達(dá)到真正的美好,我認(rèn)為C#/.NET可以衡量出它的高低,至少運(yùn)行在Windows平臺(tái)上 。
摘要
從第一部分開始,就給出全文的延伸的意思,從性能方面,如何能更好的用C#/.NET寫出很好的軟件。在第一部分里,一些結(jié)果雖然不是很值得注意,但是有很多的地方還是令人叫奇的(至少我是這樣的?。?。
這些研究結(jié)果展示了C#可以提供非常好的性能效果——至少在我們測(cè)試的范圍內(nèi)是這樣——同時(shí)不能把它如同象過(guò)去把Visual Basic和一些擴(kuò)展版本的Java當(dāng)作一種性能差的語(yǔ)言來(lái)對(duì)待。對(duì)我而言,我比較注意語(yǔ)言運(yùn)行的性能,當(dāng)然,他們的表現(xiàn)也會(huì)超出我的預(yù)期效果。我所提出的嚴(yán)肅評(píng)論是從語(yǔ)言特征的觀點(diǎn)和對(duì)多個(gè)編程范例的支持方面得出的。
作為一個(gè)最初使用C++的程序員,我從特定的觀點(diǎn)得出這些比較結(jié)果。這可以從RAII和封裝測(cè)試以及它們的語(yǔ)言解釋上看的出來(lái)。在某種意義上來(lái)說(shuō),這就好比比較蘋果和橙子,對(duì)于不同背景的人們可能會(huì)對(duì)我為什么如此強(qiáng)調(diào)模板和確定性析構(gòu)感到疑惑,沒(méi)有這些他們也進(jìn)行的很好。
毫無(wú)疑問(wèn),模板給C++帶來(lái)了非凡的革命,它能支持群組合作,或者獨(dú)立運(yùn)行,范例是無(wú)可比擬的。Java由于缺少模板和強(qiáng)制要求任何東西都是對(duì)象,而被批評(píng)了很長(zhǎng)時(shí)間。.NET框架在這方面做法一樣也很讓人失望;可能由于Java缺少模板而他們可以在.NET環(huán)境下得到(他們確實(shí)達(dá)到)讓更廣的開發(fā)團(tuán)體感到信賴并接受了它。
缺少對(duì)RAII的支持對(duì)GUIs(通用用戶接口)是非常好的,這是在嚴(yán)格的框架內(nèi)操作的軟件——即使象J2EE這樣精密復(fù)雜和高吞吐量的語(yǔ)言,和非關(guān)鍵的程序。但復(fù)雜的軟件不必要因?yàn)樗鴱?fù)雜。在最好的情況下,到處都是最終模塊,只要在Finalize()里函數(shù)里添加Close()方法就可以了。在最壞的情況下,滯緩或者隨機(jī)的資源泄漏都會(huì)導(dǎo)致系統(tǒng)的崩潰。更甚于以上的,如果OO(面向?qū)ο螅拖驝#和Java所有都是對(duì)象——是你的目的那更讓我不安,第一個(gè)要失去的就是對(duì)象自己清除自己的責(zé)任,我沒(méi)辦法理解這個(gè)。(我知道一個(gè)非常出名的C++程序員——當(dāng)然我不能告訴是誰(shuí),他告訴我當(dāng)他在課程中突出RAII和模板的重點(diǎn)時(shí),那些非使用C++的人們露出的滑稽表情,讓他感覺好象他丟失了什么東西)
D是使用垃圾回收的語(yǔ)言,默認(rèn)是非確定性析構(gòu),有很多地方與C#和Java相似,但是盡管如此,它還是兼容支持RAII的習(xí)慣同時(shí)具有模板。并且它是一個(gè)人寫的!我不明白為什么我們對(duì)Java或者C#(.NET其它語(yǔ)言也是一樣)印象如此深刻,即使有它們支持RAII和模板的缺點(diǎn),而我們又能比較這些。有可能C#/.NET成功的原因和Java一樣,有大量的,有用的庫(kù)文件(C和C++應(yīng)該從中學(xué)習(xí),D應(yīng)該去發(fā)展),和有一個(gè)強(qiáng)有力的后臺(tái)。
最后,我想說(shuō)對(duì)于所有的性能結(jié)果的比較分析,你必須明智地使用這些結(jié)果。我努力使自己公平,選擇我相信是公平和有意義的測(cè)試。但你一定要判斷的出這些例子僅代表了廣大可能性的的一小部分(不是無(wú)限意義上的),它們不是實(shí)際的程序,僅僅是測(cè)試而已并且把它簡(jiǎn)化了,其中不可避免的帶有我個(gè)人的某種偏見在里面。通過(guò)逐步的方法,我希望降低這些因素。但你在閱讀和使用這些結(jié)果的時(shí)候要保持留意。
感謝
我要感謝 Walter Bright提供給我最新的Dv0.62版本,能夠完全的測(cè)試異常事件。感謝Intel的David Hackson給我提供了C/C++編譯器。還要感謝Scott Patterson幫我選擇了切合實(shí)際的測(cè)試方法(他總是不斷的在我煩躁、偏題、愚蠢的時(shí)候提醒我)。還要感謝Eugene Gershnik 對(duì)于我的一些斷言給了我嚴(yán)厲的反駁,幫助我適當(dāng)?shù)刈⒁庖恍┓乐拐`解的說(shuō)明。
Notes and References
[1] The C++ Programming Language, Bjarne Stroustrup, Addison Wesley, 1997
[2] The Software Optimization Cookbook, Richard Gerber, Intel Press, 2002
[3] Applied Microsoft .NET Framework Programming 1 st Edition, Jeffrey Richter, Microsoft Press, 2002
[4] Described in “ Win32 Performance Measurement Options, ” Matthew Wilson, Windows Developer Network, Volume 2 Number 5, May 2003.
]]>