<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Vincent

    Vicent's blog
    隨筆 - 74, 文章 - 0, 評(píng)論 - 5, 引用 - 0
    數(shù)據(jù)加載中……

    Java 理論與實(shí)踐: 動(dòng)態(tài)編譯與性能測量

    為動(dòng)態(tài)編譯的語言(例如 Java)編寫和解釋性能評(píng)測,要比為靜態(tài)編譯的語言(例如 C 或 C++)編寫困難得多。在這期的 Java 理論與實(shí)踐 中,Brian Goetz 介紹了動(dòng)態(tài)編譯使性能測試復(fù)雜的諸多原因中的一些。請?jiān)诒疚母綆У挠懻摻M上與作者和其他讀者分享您對本文的看法。 (您也可以選擇本文頂部或底部的 討論 訪問論壇。)

    這個(gè)月,我著手撰寫一篇文章,分析一個(gè)寫得很糟糕的微評(píng)測。畢竟,我們的程序員一直受性能困擾,我們也都想了解我們編寫、使用或批評(píng)的代碼的性能特征。當(dāng)我偶然間寫到性能這個(gè)主題時(shí),我經(jīng)常得到這樣的電子郵件:“我寫的這個(gè)程序顯示,動(dòng)態(tài) frosternation 要比靜態(tài) blestification 快,與您上一篇的觀點(diǎn)相反!”許多隨這類電子郵件而來的所謂“評(píng)測“程序,或者它們運(yùn)行的方式,明顯表現(xiàn)出他們對于 JVM 執(zhí)行字節(jié)碼的實(shí)際方式缺乏基本認(rèn)識(shí)。所以,在我著手撰寫這樣一篇文章(將在未來的專欄中發(fā)表)之前,我們先來看看 JVM 幕后的東西。理解動(dòng)態(tài)編譯和優(yōu)化,是理解如何區(qū)分微評(píng)測好壞的關(guān)鍵(不幸的是,好的微評(píng)測很少)。

    動(dòng)態(tài)編譯簡史

    Java 應(yīng)用程序的編譯過程與靜態(tài)編譯語言(例如 C 或 C++)不同。靜態(tài)編譯器直接把源代碼轉(zhuǎn)換成可以直接在目標(biāo)平臺(tái)上執(zhí)行的機(jī)器代碼,不同的硬件平臺(tái)要求不同的編譯器。 Java 編譯器把 Java 源代碼轉(zhuǎn)換成可移植的 JVM 字節(jié)碼,所謂字節(jié)碼指的是 JVM 的“虛擬機(jī)器指令”。與靜態(tài)編譯器不同,javac 幾乎不做什么優(yōu)化 —— 在靜態(tài)編譯語言中應(yīng)當(dāng)由編譯器進(jìn)行的優(yōu)化工作,在 Java 中是在程序執(zhí)行的時(shí)候,由運(yùn)行時(shí)執(zhí)行。

    第一代 JVM 完全是解釋的。JVM 解釋字節(jié)碼,而不是把字節(jié)碼編譯成機(jī)器碼并直接執(zhí)行機(jī)器碼。當(dāng)然,這種技術(shù)不會(huì)提供最好的性能,因?yàn)橄到y(tǒng)在執(zhí)行解釋器上花費(fèi)的時(shí)間,比在需要運(yùn)行的程序上花費(fèi)的時(shí)間還要多。

    即時(shí)編譯

    對于證實(shí)概念的實(shí)現(xiàn)來說,解釋是合適的,但是早期的 JVM 由于太慢,迅速獲得了一個(gè)壞名聲。下一代 JVM 使用即時(shí) (JIT) 編譯器來提高執(zhí)行速度。按照嚴(yán)格的定義,基于 JIT 的虛擬機(jī)在執(zhí)行之前,把所有字節(jié)碼轉(zhuǎn)換成機(jī)器碼,但是以惰性方式來做這項(xiàng)工作:JIT 只有在確定某個(gè)代碼路徑將要執(zhí)行的時(shí)候,才編譯這個(gè)代碼路徑(因此有了名稱“ 即時(shí) 編譯”)。這個(gè)技術(shù)使程序能啟動(dòng)得更快,因?yàn)樵陂_始執(zhí)行之前,不需要冗長的編譯階段。

    JIT 技術(shù)看起來很有前途,但是它有一些不足。JIT 消除了解釋的負(fù)擔(dān)(以額外的啟動(dòng)成本為代價(jià)),但是由于若干原因,代碼的優(yōu)化等級(jí)仍然是一般般。為了避免 Java 應(yīng)用程序嚴(yán)重的啟動(dòng)延遲,JIT 編譯器必須非常迅速,這意味著它無法把大量時(shí)間花在優(yōu)化上。所以,早期的 JIT 編譯器在進(jìn)行內(nèi)聯(lián)假設(shè)(inlining assumption)方面比較保守,因?yàn)樗鼈儾恢篮竺婵赡芤b入哪個(gè)類。

    雖然從技術(shù)上講,基于 JIT 的虛擬機(jī)在執(zhí)行字節(jié)碼之前,要先編譯字節(jié)碼,但是 JIT 這個(gè)術(shù)語通常被用來表示任何把字節(jié)碼轉(zhuǎn)換成機(jī)器碼的動(dòng)態(tài)編譯過程 —— 即使那些能夠解釋字節(jié)碼的過程也算。

    HotSpot 動(dòng)態(tài)編譯

    HotSpot 執(zhí)行過程組合了編譯、性能分析以及動(dòng)態(tài)編譯。它沒有把所有要執(zhí)行的字節(jié)碼轉(zhuǎn)換成機(jī)器碼,而是先以解釋器的方式運(yùn)行,只編譯“熱門”代碼 —— 執(zhí)行得最頻繁的代碼。當(dāng) HotSpot 執(zhí)行時(shí),會(huì)搜集性能分析數(shù)據(jù),用來決定哪個(gè)代碼段執(zhí)行得足夠頻繁,值得編譯。只編譯執(zhí)行最頻繁的代碼有幾項(xiàng)性能優(yōu)勢:沒有把時(shí)間浪費(fèi)在編譯那些不經(jīng)常執(zhí)行的代碼上;這樣,編譯器就可以花更多時(shí)間來優(yōu)化熱門代碼路徑,因?yàn)樗涝谶@上面花的時(shí)間物有所值。而且,通過延遲編譯,編譯器可以訪問性能分析數(shù)據(jù),并用這些數(shù)據(jù)來改進(jìn)優(yōu)化決策,例如是否需要內(nèi)聯(lián)某個(gè)方法調(diào)用。

    為了讓事情變得更復(fù)雜,HotSpot 提供了兩個(gè)編譯器:客戶機(jī)編譯器和服務(wù)器編譯器。默認(rèn)采用客戶機(jī)編譯器;在啟動(dòng) JVM 時(shí),您可以指定 -server 開關(guān),選擇服務(wù)器編譯器。服務(wù)器編譯器針對最大峰值操作速度進(jìn)行了優(yōu)化,適用于需要長期運(yùn)行的服務(wù)器應(yīng)用程序。客戶機(jī)編譯器的優(yōu)化目標(biāo),是減少應(yīng)用程序的啟動(dòng)時(shí)間和內(nèi)存消耗,優(yōu)化的復(fù)雜程度遠(yuǎn)遠(yuǎn)低于服務(wù)器編譯器,因此需要的編譯時(shí)間也更少。

    HotSpot 服務(wù)器編譯器能夠執(zhí)行各種樣的類。它能夠執(zhí)行許多靜態(tài)編譯器中常見的標(biāo)準(zhǔn)優(yōu)化,例如代碼提升( hoisting)、公共的子表達(dá)式清除、循環(huán)展開(unrolling)、范圍檢測清除、死代碼清除、數(shù)據(jù)流分析,還有各種在靜態(tài)編譯語言中不實(shí)用的優(yōu)化技術(shù),例如虛方法調(diào)用的聚合內(nèi)聯(lián)。

    持續(xù)重新編譯

    HotSpot 技術(shù)另一個(gè)有趣的方面是:編譯不是一個(gè)全有或者全無(all-or-nothing)的命題。在解釋代碼路徑一定次數(shù)之后,會(huì)把它重新編譯成機(jī)器碼。但是 JVM 會(huì)繼續(xù)進(jìn)行性能分析,而且如果認(rèn)為代碼路徑特別熱門,或者未來的性能分析數(shù)據(jù)認(rèn)為存在額外的優(yōu)化可能,那么還有可能用更高一級(jí)的優(yōu)化重新編譯代碼。JVM 在一個(gè)應(yīng)用程序的執(zhí)行過程中,可能會(huì)把相同的字節(jié)碼重新編譯許多次。為了深入了解編譯器做了什么,請用 -XX:+PrintCompilation 標(biāo)志調(diào)用 JVM,這個(gè)標(biāo)志會(huì)使編譯器(客戶機(jī)或服務(wù)器)每次運(yùn)行的時(shí)候打印一條短消息。

    棧上(On-stack)替換

    HotSpot 開始的版本編譯的時(shí)候每次編譯一個(gè)方法。如果某個(gè)方法的累計(jì)執(zhí)行次數(shù)超過指定的循環(huán)迭代次數(shù)(在 HotSpot 的第一版中,是 10,000 次),那么這個(gè)方法就被當(dāng)作熱門方法,計(jì)算的方式是:為每個(gè)方法關(guān)聯(lián)一個(gè)計(jì)數(shù)器,每次執(zhí)行一個(gè)后向分支時(shí),就會(huì)遞增計(jì)數(shù)器一次。但是,在方法編譯之后,方法調(diào)用并沒有切換到編譯的版本,需要退出并重新進(jìn)入方法,后續(xù)調(diào)用才會(huì)使用編譯的版本。結(jié)果就是,在某些情況下,可能永遠(yuǎn)不會(huì)用到編譯的版本,例如對于計(jì)算密集型程序,在這類程序中所有的計(jì)算都是在方法的一次調(diào)用中完成的。重量級(jí)方法可能被編譯,但是編譯的代碼永遠(yuǎn)用不到。

    HotSpot 最近的版本采用了稱為 棧上(on-stack)替換 (OSR) 的技術(shù),支持在循環(huán)過程中間,從解釋執(zhí)行切換到編譯的代碼(或者從編譯代碼的一個(gè)版本切換到另一個(gè)版本)。





    回頁首


    那么,這與評(píng)測有什么關(guān)系?

    我向您許諾了一篇關(guān)于評(píng)測和性能測量的文章,但是迄今為止,您得到的只是歷史的教訓(xùn)和 Sun 的 HotSpot 白皮書的老調(diào)重談。繞這么大的圈子的原因是,如果不理解動(dòng)態(tài)編譯的過程,就不可能正確地編寫或解釋 Java 類的性能測試。(即使深入理解動(dòng)態(tài)編譯和 JVM 優(yōu)化,也仍然是非常困難的。)

    為 Java 代碼編寫微評(píng)測遠(yuǎn)比為 C 代碼編寫難得多

    判斷方法 A 是否比方法 B 更快的傳統(tǒng)方法,是編寫小的評(píng)測程序,通常叫做 微評(píng)測。這個(gè)趨勢非常有意義。科學(xué)的方法不能缺少獨(dú)立的調(diào)查。魔鬼總在細(xì)節(jié)之中。為動(dòng)態(tài)編譯的語言編寫并解釋評(píng)測,遠(yuǎn)比為靜態(tài)編譯的語言難得多。為了了解某個(gè)結(jié)構(gòu)的性能,編寫一個(gè)使用該結(jié)構(gòu)的程序一點(diǎn)也沒有錯(cuò),但是在許多情況下,用 Java 編寫的微評(píng)測告訴您的,往往與您所認(rèn)為的不一樣。

    使用 C 程序時(shí),您甚至不用運(yùn)行它,就能了解許多程序可能的性能特征。只要看看編譯出的機(jī)器碼就可以了。編譯器生成的指令就是將要執(zhí)行的機(jī)器碼,一般情況下,可以很合理地理解它們的時(shí)間特征。(有許多有毛病的例子,因?yàn)榭偸沁z漏分支預(yù)測或緩存,所以性能差的程度遠(yuǎn)遠(yuǎn)超過查看機(jī)器碼所能夠想像的程度,但是大多數(shù)情況下,您都可以通過查看機(jī)器碼了解 C 程序的性能的很多方面。)

    如果編譯器認(rèn)為某段代碼不恰當(dāng),準(zhǔn)備把它優(yōu)化掉(通常的情況是,評(píng)測到它實(shí)際上不做任何事情),那么您在生成的機(jī)器碼中可以看到這個(gè)優(yōu)化 —— 代碼不在那兒了。通常,對于 C 代碼,您不必執(zhí)行很長時(shí)間,就可以對它的性能做出合理的推斷。

    而在另一方面,HotSpot JIT 在程序運(yùn)行時(shí)會(huì)持續(xù)地把 Java 字節(jié)碼重新編譯成機(jī)器碼,而重新編譯觸發(fā)的次數(shù)無法預(yù)期,觸發(fā)重新編譯的依據(jù)是性能分析數(shù)據(jù)積累到一定數(shù)量、裝入新類,或者執(zhí)行到的代碼路徑的類已經(jīng)裝入,但是還沒有執(zhí)行過。持續(xù)的重新編譯情況下的時(shí)間測量會(huì)非常混亂、讓人誤解,而且要想獲得有用的性能數(shù)據(jù),通常必須讓 Java 代碼運(yùn)行相當(dāng)長的時(shí)間(我曾經(jīng)看到過一些怪事,在程序啟動(dòng)運(yùn)行之后要加速幾個(gè)小時(shí)甚至數(shù)天),才能獲得有用的性能數(shù)據(jù)。





    回頁首


    清除死代碼

    編寫好評(píng)測的一個(gè)挑戰(zhàn)就是,優(yōu)化編譯器要擅長找出死代碼 —— 對于程序執(zhí)行的輸出沒有作用的代碼。但是評(píng)測程序一般不產(chǎn)生任何輸出,這就意味著有一些,或者全部代碼都有可能被優(yōu)化掉,而毫無知覺,這時(shí)您實(shí)際測量的執(zhí)行要少于您設(shè)想的數(shù)量。具體來說,許多微評(píng)測在用 -server 方式運(yùn)行時(shí),要比用 -client 方式運(yùn)行時(shí)好得多,這不是因?yàn)榉?wù)器編譯器更快(雖然服務(wù)器編譯器一般更快),而是因?yàn)榉?wù)器編譯器更擅長優(yōu)化掉死代碼。不幸的是,能夠讓您的評(píng)測工作非常短(可能會(huì)把評(píng)測完全優(yōu)化掉)的死代碼優(yōu)化,在處理實(shí)際做些工作的代碼時(shí),做得就不會(huì)那么好了。

    奇怪的結(jié)果

    清單 1 的評(píng)測包含一個(gè)什么也不做的代碼塊,它是從一個(gè)測試并發(fā)線程性能的評(píng)測中摘出來的,但是它實(shí)際測量的根本不是要評(píng)測的東西。(這個(gè)示例是從 JavaOne 2003 的演示 “The Black Art of Benchmarking” 中借用的。請參閱 參考資料。)


    清單 1. 被意料之外的死代碼弄亂的評(píng)測
    												
    																		
            
    public class StupidThreadTest {
        public static void doSomeStuff() {
            double uselessSum = 0;
            for (int i=0; i<1000; i++) {
                for (int j=0;j<1000; j++) {
                    uselessSum += (double) i + (double) j;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            doSomeStuff();
            
            int nThreads = Integer.parseInt(args[0]);
            Thread[] threads = new Thread[nThreads];
            for (int i=0; i<nThreads; i++)
                threads[i] = new Thread(new Runnable() {
                    public void run() { doSomeStuff(); }
                });
            long start = System.currentTimeMillis();
            for (int i = 0; i < threads.length; i++)
                threads[i].start();
            for (int i = 0; i < threads.length; i++)
                threads[i].join();
            long end = System.currentTimeMillis();
            System.out.println("Time: " + (end-start) + "ms");
        }
    }
    
          
    												
    										

    表面上看, doSomeStuff() 方法可以給線程分點(diǎn)事做,所以我們能夠從 StupidThreadBenchmark 的運(yùn)行時(shí)間推導(dǎo)出多線程調(diào)度開支的一些情況。但是,因?yàn)?uselessSum 從沒被用過,所以編譯器能夠判斷出 doSomeStuff 中的全部代碼是死的,然后把它們?nèi)績?yōu)化掉。一旦循環(huán)中的代碼消失,循環(huán)也就消失了,只留下一個(gè)空空如也的 doSomeStuff。表 1 顯示了使用客戶機(jī)和服務(wù)器方式執(zhí)行 StupidThreadBenchmark 的性能。兩個(gè) JVM 運(yùn)行大量線程的時(shí)候,都表現(xiàn)出差不多是線性的運(yùn)行時(shí)間,這個(gè)結(jié)果很容易被誤解為服務(wù)器 JVM 比客戶機(jī) JVM 快 40 倍。而實(shí)際上,是服務(wù)器編譯器做了更多優(yōu)化,發(fā)現(xiàn)整個(gè) doSomeStuff 是死代碼。雖然確實(shí)有許多程序在服務(wù)器 JVM 上會(huì)提速,但是您在這里看到的提速僅僅代表一個(gè)寫得糟糕的評(píng)測,而不能成為服務(wù)器 JVM 性能的證明。但是如果您沒有細(xì)看,就很容易會(huì)把兩者混淆。


    表 1. 在客戶機(jī)和服務(wù)器 JVM 中 StupidThreadBenchmark 的性能
    線程數(shù)量 客戶機(jī) JVM 運(yùn)行時(shí)間 服務(wù)器 JVM 運(yùn)行時(shí)間
    10 43 2
    100 435 10
    1000 4142 80
    10000 42402 1060

    對于評(píng)測靜態(tài)編譯語言來說,處理過于積極的死代碼清除也是一個(gè)問題。但是,在靜態(tài)編譯語言中,能夠更容易地發(fā)現(xiàn)編譯器清除了大塊評(píng)測。您可以查看生成的機(jī)器碼,查看是否漏了某塊程序。而對于動(dòng)態(tài)編譯語言,這些信息不太容易訪問得到。





    回頁首


    預(yù)熱

    如果您想測量 X 的性能,一般情況下您是想測量它編譯后的性能,而不是它的解釋性能(您想知道 X 在賽場上能跑多快)。要做到這樣,需要“預(yù)熱” JVM —— 即讓目標(biāo)操作執(zhí)行足夠的時(shí)間,這樣編譯器在為執(zhí)行計(jì)時(shí)之前,就有足夠的運(yùn)行解釋的代碼,并用編譯的代碼替換解釋代碼。

    使用早期 JIT 和沒有棧上替換的動(dòng)態(tài)編譯器,有一個(gè)容易的公式可以測量方法編譯后的性能:運(yùn)行多次調(diào)用,啟動(dòng)計(jì)時(shí)器,然后執(zhí)行若干次方法。如果預(yù)熱調(diào)用超過方法被編譯的閾值,那么實(shí)際計(jì)時(shí)的調(diào)用就有可能全部是編譯代碼執(zhí)行的時(shí)間,所有的編譯開支應(yīng)當(dāng)在開始計(jì)時(shí)之前發(fā)生。

    而使用今天的動(dòng)態(tài)編譯器,事情更困難。編譯器運(yùn)行的次數(shù)很難預(yù)測,JVM 按照自己的想法從解釋代碼切換到編譯代碼,而且在運(yùn)行期間,相同的代碼路徑可能編譯、重新編譯不止一次。如果您不處理這些事件的計(jì)時(shí)問題,那么它們會(huì)嚴(yán)重歪曲您的計(jì)時(shí)結(jié)果。

    圖 1 顯示了由于預(yù)計(jì)不到的動(dòng)態(tài)編譯而造成的可能的計(jì)時(shí)歪曲。假設(shè)您正在通過循環(huán)計(jì)時(shí) 200,000 次迭代,編譯代碼比解釋代碼快 10 倍。如果編譯只在 200,000 次迭代時(shí)才發(fā)生,那么您測量的只是解釋代碼的性能(時(shí)間線(a))。如果編譯在 100,000 次迭代時(shí)發(fā)生,那么您總共的運(yùn)行時(shí)間是運(yùn)行 200,000 次解釋迭代的時(shí)間,加上編譯時(shí)間(編譯時(shí)間非您所愿),加上執(zhí)行 100,000 次編譯迭代的時(shí)間(時(shí)間線(b))。如果編譯在 20,000 次迭代時(shí)發(fā)生,那么總時(shí)間會(huì)是 20,000 次解釋迭代,加上編譯時(shí)間,再加上 180,000 次編譯迭代(時(shí)間線(c))。因?yàn)槟恢谰幾g器什么時(shí)候執(zhí)行,也不知道要執(zhí)行多長時(shí)間,所以您可以看到,您的測量可能受到嚴(yán)重的歪曲。根據(jù)編譯時(shí)間和編譯代碼比解釋代碼快的程度,即使對迭代數(shù)量只做很小的變化,也可能造成測量的“性能”有極大差異。


    圖 1. 因?yàn)閯?dòng)態(tài)編譯計(jì)時(shí)造成的性能測量歪曲
    時(shí)間線圖

    那么,到底多少預(yù)熱才足夠呢?您不知道。您能做到的最好的,就是用 -XX:+PrintCompilation 開關(guān)來運(yùn)行評(píng)測,觀察什么造成編譯器工作,然后改變評(píng)測程序的結(jié)構(gòu),以確保編譯在您啟動(dòng)計(jì)時(shí)之前發(fā)生,在計(jì)時(shí)循環(huán)過程中不會(huì)再發(fā)生編譯。

    不要忘記垃圾收集

    那么,您已經(jīng)看到,如果您想得到正確的計(jì)時(shí)結(jié)果,就必須要讓被測代碼比您想像的多運(yùn)行幾次,以便讓 JVM 預(yù)熱。另一方面,如果測試代碼要進(jìn)行對象分配工作(差不多所有的代碼都要這樣),那么垃圾收集器也肯定會(huì)運(yùn)行。這是會(huì)嚴(yán)重歪曲計(jì)時(shí)結(jié)果的另一個(gè)因素 —— 即使對迭代數(shù)量只做很小的變化,也意味著沒有垃圾收集和有垃圾收集之間的區(qū)別,就會(huì)偏離“每迭代時(shí)間”的測量。

    如果用 -verbose:gc 開關(guān)運(yùn)行評(píng)測,您可以看到在垃圾收集上耗費(fèi)了多少時(shí)間,并相應(yīng)地調(diào)整您的計(jì)時(shí)數(shù)據(jù)。更好一些的話,您可以長時(shí)間運(yùn)行您的程序,這可以保證觸發(fā)許多垃圾收集,從而更精確地分?jǐn)偫占某杀尽?





    回頁首


    動(dòng)態(tài)反優(yōu)化(deoptimization)

    許多標(biāo)準(zhǔn)的優(yōu)化只能在“基本塊”內(nèi)執(zhí)行,所以內(nèi)聯(lián)方法調(diào)用對于達(dá)到好的優(yōu)化通常很重要。通過內(nèi)聯(lián)方法調(diào)用,不僅方法調(diào)用的開支被清除,而且給優(yōu)化器提供了更大的優(yōu)化塊可以優(yōu)化,會(huì)帶來相當(dāng)大的死代碼優(yōu)化機(jī)會(huì)。

    清單 2 顯示了一個(gè)通過內(nèi)聯(lián)實(shí)現(xiàn)的這類優(yōu)化的示例。 outer() 方法用參數(shù) null 調(diào)用 inner(),結(jié)果是 inner() 什么也不做。但是通過把 inner() 的調(diào)用內(nèi)聯(lián),編譯器可以發(fā)現(xiàn) inner()else 分支是死的,因此能夠把測試和 else 分支優(yōu)化掉,在某種程度上,它甚至能把整個(gè)對 inner() 的調(diào)用全優(yōu)化掉。如果 inner() 沒有被內(nèi)聯(lián),那么這個(gè)優(yōu)化是不可能發(fā)生的。


    清單 2. 內(nèi)聯(lián)如何帶來更好的死代碼優(yōu)化
    												
    																		
            
    public class Inline {
      public final void inner(String s) {
        if (s == null)
          return;
        else {
          // do something really complicated
        }
      }
    
      public void outer() {
        String s=null; 
        inner(s);
      }
    }
    
          
    												
    										

    但是不方便的是,虛方法對內(nèi)聯(lián)造成了障礙,而虛函數(shù)調(diào)用在 Java 中要比在 C++ 中普遍。假設(shè)編譯器正試圖優(yōu)化以下代碼中對 doSomething() 的調(diào)用:

    												
    														  Foo foo = getFoo();
      foo.doSomething(); 
    
    												
    										

    從這個(gè)代碼片斷中,編譯器沒有必要分清要執(zhí)行哪個(gè)版本的 doSomething() —— 是在類 Foo 中實(shí)現(xiàn)的版本,還是在 Foo 的子類中實(shí)現(xiàn)的版本?只在少數(shù)情況下答案才明顯 —— 例如 Foofinal 的,或者 doSomething()Foo 中被定義為 final 方法 —— 但是在多數(shù)情況下,編譯器不得不猜測。對于每次只編譯一個(gè)類的靜態(tài)編譯器,我們很幸運(yùn)。但是動(dòng)態(tài)編譯器可以使用全局信息進(jìn)行更好的決策。假設(shè)有一個(gè)還沒有裝入的類,它擴(kuò)展了應(yīng)用程序中的 Foo。現(xiàn)在的情景更像是 doSomething()Foo 中的 final 方法 —— 編譯器可以把虛方法調(diào)用轉(zhuǎn)換成一個(gè)直接分配(已經(jīng)是個(gè)改進(jìn)了),而且,還可以內(nèi)聯(lián) doSomething()。(把虛方法調(diào)用轉(zhuǎn)換成直接方法調(diào)用,叫做 單形(monomorphic)調(diào)用變換。)

    請稍等 —— 類可以動(dòng)態(tài)裝入。如果編譯器進(jìn)行了這樣的優(yōu)化,然后裝入了一個(gè)擴(kuò)展了 Foo 的類,會(huì)發(fā)生什么?更糟的是,如果這是在工廠方法 getFoo() 內(nèi)進(jìn)行的會(huì)怎么樣? getFoo() 會(huì)返回新的 Foo 子類的實(shí)例?那么,生成的代碼不就無效了么?對,是無效了。但是 JVM 能指出這個(gè)錯(cuò)誤,并根據(jù)目前無效的假設(shè),取消生成的代碼,并恢復(fù)解釋(或者重新編譯不正確的代碼路徑)。

    結(jié)果就是,編譯器要進(jìn)行主動(dòng)的內(nèi)聯(lián)決策,才能得到更高的性能,然后當(dāng)這些決策依據(jù)的假設(shè)不再有效時(shí),就會(huì)收回這些決策。實(shí)際上,這個(gè)優(yōu)化如此有效,以致于給那些不被覆蓋的方法添加 final 關(guān)鍵字(一種性能技巧,在以前的文章中建議過)對于提高實(shí)際性能沒有太大作用。

    奇怪的結(jié)果

    清單 3 中包含一個(gè)代碼模式,其中組合了不恰當(dāng)?shù)念A(yù)熱、單形調(diào)用變換以及反優(yōu)化,因此生成的結(jié)果毫無意義,而且容易被誤解:


    清單 3. 測試程序的結(jié)果被單形調(diào)用變換和后續(xù)的反優(yōu)化歪曲
    												
    																		
            
    public class StupidMathTest {
        public interface Operator {
            public double operate(double d);
        }
    
        public static class SimpleAdder implements Operator {
            public double operate(double d) {
                return d + 1.0;
            }
        }
    
        public static class DoubleAdder implements Operator {
            public double operate(double d) {
                return d + 0.5 + 0.5;
            }
        }
    
        public static class RoundaboutAdder implements Operator {
            public double operate(double d) {
                return d + 2.0 - 1.0;
            }
        }
    
        public static void runABunch(Operator op) {
            long start = System.currentTimeMillis();
            double d = 0.0;
            for (int i = 0; i < 5000000; i++)
                d = op.operate(d);
            long end = System.currentTimeMillis();
            System.out.println("Time: " + (end-start) + "   ignore:" + d);
        }
    
        public static void main(String[] args) {
            Operator ra = new RoundaboutAdder();
            runABunch(ra); // misguided warmup attempt
            runABunch(ra);
            Operator sa = new SimpleAdder();
            Operator da = new DoubleAdder();
            runABunch(sa);
            runABunch(da);
        }
    }
    
          
    												
    										

    StupidMathTest 首先試圖做些預(yù)熱(沒有成功),然后測量 SimpleAdderDoubleAdderRoundaboutAdder 的運(yùn)行時(shí)間,結(jié)果如表 2 所示。看起來好像先加 1,再加 2 ,然后再減 1 最快。加兩次 0.5 比加 1 還快。這有可能么?(答案是:不可能。)


    表 2. StupidMathTest 毫無意義且令人誤解的結(jié)果
    方法 運(yùn)行時(shí)間
    SimpleAdder 88ms
    DoubleAdder 76ms
    RoundaboutAdder 14ms

    這里發(fā)生什么呢?在預(yù)熱循環(huán)之后, RoundaboutAdderrunABunch() 確實(shí)已經(jīng)被編譯了,而且編譯器 OperatorRoundaboutAdder 上進(jìn)行了單形調(diào)用轉(zhuǎn)換,第一輪運(yùn)行得非常快。而在第二輪( SimpleAdder)中,編譯器不得不反優(yōu)化,又退回虛函數(shù)分配之中,所以第二輪的執(zhí)行表現(xiàn)得更慢,因?yàn)椴荒馨烟摵瘮?shù)調(diào)用優(yōu)化掉,把時(shí)間花在了重新編譯上。在第三輪( DoubleAdder)中,重新編譯比第二輪少,所以運(yùn)行得就更快。(在現(xiàn)實(shí)中,編譯器會(huì)在 RoundaboutAdderDoubleAdder 上進(jìn)行常數(shù)替換(constant folding),生成與 SimpleAdder 幾乎相同的代碼。所以如果在運(yùn)行時(shí)間上有差異,那么不是因?yàn)樗阈g(shù)代碼)。哪個(gè)代碼首先執(zhí)行,哪個(gè)代碼就會(huì)最快。

    那么,從這個(gè)“評(píng)測”中,我們能得出什么結(jié)論呢?實(shí)際上,除了評(píng)測動(dòng)態(tài)編譯語言要比您可能想到的要微妙得多之外,什么也沒得到。





    回頁首


    結(jié)束語

    這個(gè)示例中的結(jié)果錯(cuò)得如此明顯,所以很清楚,肯定發(fā)生了什么,但是更小的結(jié)果能夠很容易地歪曲您的性能測試程序的結(jié)果,卻不會(huì)觸發(fā)您的“這里肯定有什么東西有問題”的警惕。雖然本文列出的這些內(nèi)容是微評(píng)測歪曲的一般來源,但是還有許多其他來源。本文的中心思想是:您正在測量的,通常不是您以為您正在測量的。實(shí)際上,您通常所測量的,不是您以為您正在測量的。對于那些沒有包含什么實(shí)際的程序負(fù)荷,測試時(shí)間不夠長的性能測試的結(jié)果,一定要非常當(dāng)心。

    posted on 2006-08-24 17:36 Binary 閱讀(234) 評(píng)論(0)  編輯  收藏 所屬分類: j2se

    主站蜘蛛池模板: 免费在线观看亚洲| 日韩精品人妻系列无码专区免费| 亚洲成a∨人片在无码2023 | 女人18毛片免费观看| 青青青国产在线观看免费网站 | 久久久免费的精品| 免费网站看av片| 欧洲人免费视频网站在线| 久久99精品免费视频| 亚洲视频在线免费观看| 最近2019中文字幕免费大全5| 1000部免费啪啪十八未年禁止观看| 2021国内精品久久久久精免费| 久草免费在线观看视频| 黄页网站在线看免费| 成人免费无码视频在线网站| 波多野结衣久久高清免费| 免费在线精品视频| 国产午夜亚洲精品理论片不卡| 亚洲中文字幕无码日韩| 亚洲国产人成在线观看69网站| 亚洲欧洲日本精品| 亚洲精品无码你懂的| 日韩精品无码永久免费网站| 日本不卡免费新一区二区三区| 最近中文字幕免费2019| 四虎在线免费播放| 亚洲国产精品尤物YW在线观看| 亚洲国产成人片在线观看| 亚洲日产2021三区在线 | 日本亚洲成高清一区二区三区| 亚洲AV第一页国产精品| 亚洲人成日本在线观看| 美女视频黄频a免费观看| 99久久99这里只有免费的精品| 91免费国产精品| 国产精品深夜福利免费观看| 亚洲一区精品无码| 久久精品国产99国产精品亚洲| 337P日本欧洲亚洲大胆艺术图| 中国性猛交xxxxx免费看|