技術(shù) | 時(shí)間(單位:納秒) |
---|---|
String |
69,809,420 |
StringBuffer |
251,652,393 |
Rope.charAt |
79,441,772,895 |
Rope.iterator |
1,910,836,551 |
用來(lái)得到表 2 中符合預(yù)期的結(jié)果的 rope,是通過(guò)對(duì)初始字符串執(zhí)行一系列復(fù)雜的變換之后創(chuàng)建的。但是,如果直接從字符序列創(chuàng)建這個(gè) rope 而不進(jìn)行變換,那么性能數(shù)字會(huì)產(chǎn)生較大的變化。表 3 比較了兩種方法的性能。這次是在一個(gè)包含 182,029 個(gè)字符的 rope 上迭代,這次的 rope 是直接用包含 Project Gutenberg 版查理斯·狄更斯 圣誕歡歌的字符數(shù)組初始化的。
技術(shù) | 時(shí)間(單位:納秒) |
---|---|
String |
602,162 |
StringBuffer |
2,259,917 |
Rope.charAt |
462,904 |
Rope.iterator |
3,466,047 |
如何用我前面的理論討論解釋這一性能逆轉(zhuǎn)呢? rope 的構(gòu)建是個(gè)關(guān)鍵因素:當(dāng)直接使用底層的 CharacterSequence或字符數(shù)組構(gòu)建 rope 時(shí),它只有一個(gè)簡(jiǎn)單的結(jié)構(gòu),里面包含一個(gè)扁平 rope。因?yàn)檫@個(gè) rope 不包含連接或子串節(jié)點(diǎn),所以字符查詢操作要么直接委托給底層序列的 charAt方法(在 CharacterSequence的情況下),要么包含進(jìn)行數(shù)組查詢(在數(shù)組的情況下)。扁平 rope 的 Rope.charAt性能與底層表示的 chatAt 性能匹配,通常是 O(1);所以性能是不同的。
聰明的讀者可能想知道既然大家都提供 0(1)的訪問(wèn)時(shí)間,為什么 charAt會(huì)比迭代器快 7 倍。這個(gè)區(qū)別是因?yàn)樵?Java 語(yǔ)言中,Iterator必須返回 Object。而 charAt實(shí)現(xiàn)直接返回 char原語(yǔ),迭代器實(shí)現(xiàn)必須將每個(gè) char裝箱為一個(gè) Character對(duì)象。自動(dòng)裝箱可能消除了原語(yǔ)裝箱在語(yǔ)法上的麻煩,但是不能消除性能上的損失。
后,非常明顯的是:Rope.charAt的性能比 String.charAt的性能好。原因是 Rope使用專門的類來(lái)表示延遲子串(lazy substring),因此使普通 rope 的 charAt的實(shí)現(xiàn)保持簡(jiǎn)單。對(duì)比之下,Java SE 的 String實(shí)現(xiàn)使用同一個(gè)類表示常規(guī)字符串和延遲子串,因此多多少少將 charAt的邏輯弄得復(fù)雜起來(lái),所以在迭代常規(guī)字符串期間增加了少量性能開(kāi)支。
在討論 rope 上的正則表達(dá)式搜索性能的優(yōu)化時(shí),還會(huì)提到 Rope 迭代。
用 Rope.write 對(duì)輸出進(jìn)行優(yōu)化
在某種程度上,多數(shù) rope 實(shí)例都必須寫(xiě)入到某些位置,通常寫(xiě)到一個(gè)流內(nèi)。不幸的是,將對(duì)象寫(xiě)入流要調(diào)用對(duì)象的 toString方法。這種序列化方式強(qiáng)行在寫(xiě)入一個(gè)字符之前在內(nèi)存中建立整個(gè)對(duì)象的字符串表示,對(duì)于大型對(duì)象來(lái)說(shuō),這是個(gè)非常大的性能拖累。Ropes for Java 設(shè)計(jì)的時(shí)候考慮了大型的字符串對(duì)象,所以它提供了更好的方式。
為了提高性能,Rope引入了一個(gè) write 方法,接受一個(gè) Writer和一個(gè)范圍說(shuō)明,然后將 rope 的指定范圍的字符寫(xiě)到 writer。這樣節(jié)省了用 rope 構(gòu)建 String的時(shí)間和內(nèi)存開(kāi)支,對(duì)于大型 rope 來(lái)說(shuō),這種開(kāi)支是相當(dāng)巨大的。清單 4 顯示了 rope 輸出的標(biāo)準(zhǔn)方式和增強(qiáng)方式。
清單 4. Rope輸出的兩種方式
out.write(rope);
rope.write(out);
表 4 包含的測(cè)評(píng)結(jié)果是將一個(gè)長(zhǎng)度為 10,690,488、深度為 65 的 rope 寫(xiě)入一個(gè)由內(nèi)存緩沖區(qū)支持的流的結(jié)果。請(qǐng)注意,只顯示了節(jié)省的時(shí)間,但是在臨時(shí)分配的堆空間上的節(jié)省也非常巨大。
技術(shù) | 時(shí)間(單位:納秒) |
---|---|
out.write |
75,136,884 |
rope.write |
13,591,023 |
變換性能測(cè)評(píng)
Rope 的變換從理論上要比使用連續(xù)字符的字符串表示方式快得多。但反過(guò)來(lái),正如所看到的,rope 的迭代變慢了。在這一節(jié),將看到 Ropes for Java 和 String、StringBuffer進(jìn)行變換操作的性能測(cè)評(píng)比較。
全部測(cè)試都用 Project Gutenberg 版本的電子書(shū) 圣誕歡歌初始化(182,029 個(gè)字節(jié)),并對(duì)其連續(xù)應(yīng)用一系列變換。在多數(shù)情況下,變換的數(shù)量在 20 到 480 的范圍內(nèi)變動(dòng),以便演示數(shù)據(jù)結(jié)構(gòu)縮放的效果。(圖 2、3、4 將變換的數(shù)量稱為 計(jì)劃長(zhǎng)度(plan length)。)每個(gè)測(cè)試執(zhí)行了七次,并使用中間結(jié)果。
插入性能測(cè)評(píng)
在插入測(cè)評(píng)中,從前一次迭代的輸出中隨機(jī)選擇一個(gè)子串,然后插入到字符串的隨機(jī)位置。圖 2 顯示了測(cè)評(píng)的結(jié)果。
對(duì) String和 StringBuffer來(lái)說(shuō),完成測(cè)評(píng)所需要的時(shí)間隨著計(jì)劃長(zhǎng)度的增加呈指數(shù)級(jí)增長(zhǎng)。相比之下,Rope 則執(zhí)行得極好。
附加字符串性能評(píng)測(cè)