String連接性能

          Posted on 2010-01-23 00:35 周舒陽(yáng) 閱讀(2680) 評(píng)論(7)  編輯  收藏
                新Blog開(kāi)張了!

                我就職于Liferay軟件有限公司,從事Liferay Portal的核心開(kāi)發(fā)。本著開(kāi)源,推廣技術(shù)的思想我開(kāi)始創(chuàng)建這個(gè)Blog,首先說(shuō)明一下這個(gè)Blog將主要作為我在公司網(wǎng)站上Blog的鏡像,也就是說(shuō)大部分的內(nèi)容源自http://www.liferay.com/web/shuyang.zhou/blog。我會(huì)把內(nèi)容翻譯成中文,同時(shí)也會(huì)做一些小的改動(dòng)以方便國(guó)內(nèi)的朋友。
          我主要專(zhuān)注于Java高性能運(yùn)算,并發(fā)處理,架構(gòu)設(shè)計(jì)相關(guān)方向。歡迎有相同興趣的朋友來(lái)相互溝通、學(xué)習(xí)。
          不羅嗦了,開(kāi)始正題。

          本期Blog原文參見(jiàn):
          http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/string-performance

                Java中的String是一個(gè)非常特殊的類(lèi),使它特殊的一個(gè)主要原因是:String是不可變的(immutable)。
              
                String的不可變性是Java安全機(jī)制和線(xiàn)程安全的基石,沒(méi)了它Java將變的不堪一擊。

                但不可變性的代價(jià)是昂貴的,當(dāng)你試圖“改變”一個(gè)String時(shí),你實(shí)際上是在創(chuàng)建一個(gè)新的String,而原來(lái)的那個(gè)String在大多數(shù)情況下將會(huì)成為垃圾(garbage)。多虧有了Java的垃圾自動(dòng)回收機(jī)制,開(kāi)發(fā)者不必在這些String垃圾上操太多心。但如果你完全忽略這些垃圾的存在,甚至肆意亂用String的api,你的程序無(wú)疑將遭受大量GC(垃圾回收)活動(dòng)的困擾。

                在JDK的發(fā)展史中,人們做過(guò)一些努力去改善String的垃圾創(chuàng)建開(kāi)銷(xiāo)。JDK1.0中加入StringBuffer,JDK1.5中加入StringBuilder。StringBuffer和StringBuilder在功能上是完全相同的,為一的不同點(diǎn)在于StringBuffer是線(xiàn)程安全的,而StringBuilder不是。絕大多數(shù)的String連接操作發(fā)生在一個(gè)方法調(diào)用中,也就是說(shuō)是單一線(xiàn)程的工作環(huán)境,所以線(xiàn)程安全在這里是絕對(duì)多余的。所以JDK給開(kāi)發(fā)者的建議是當(dāng)你要做String連接操作時(shí),請(qǐng)使用StringBuffer或StringBuilder,當(dāng)你確定連接操作只發(fā)生在單一線(xiàn)程環(huán)境下時(shí),使用StringBuilder而不是StringBuffer。在大多數(shù)情況下遵守這一建議與直接使用String.concat()相比能夠大幅提高性能,但實(shí)際環(huán)境中某些情況遠(yuǎn)比這復(fù)雜。這一建議并不能給你最佳的性能收益!今天我們要深入的探討一下String連接操作的性能問(wèn)題,希望能幫助大家徹底理解這一問(wèn)題。

                首先,需要辟謠,有些人說(shuō)SB(StringBuffer和StringBuilder)總是比String.concat()有更好的性能。這一說(shuō)法是不準(zhǔn)確的!在特定條件下String.concat()要?jiǎng)龠^(guò)SB。我們來(lái)通過(guò)一個(gè)例子證明這一點(diǎn)。

          任務(wù):
                連接兩個(gè)String,
                 String a = "abcdefghijklmnopq"//length=17
                 String b = "abcdefghijklmnopqr"//length=18

          說(shuō)明:
                我們將要來(lái)分析一下不同連接方案的垃圾生產(chǎn)情況。討論中我們將忽略由輸入?yún)?shù)引起的垃圾,因?yàn)樗麄儾皇怯蛇B接代碼創(chuàng)建的。另外我們只計(jì)算String內(nèi)部的char[],因?yàn)槌诉@個(gè)字符數(shù)組String的其它域都非常小,完全可以忽略他們對(duì)GC的影響。

          方案1:
                使用String.concat()

          代碼:
                 String result = a.concat(b);
                這行代碼簡(jiǎn)單到不能再簡(jiǎn)單了,不過(guò)還是讓我們來(lái)看看Sun JDK java.lang.String的源代碼,搞清楚這個(gè)調(diào)用究竟是怎樣進(jìn)行的。
          Sun JDK java.lang.String的源代碼片段:
           1     public String concat(String str) {
           2         int otherLen = str.length();
           3         if (otherLen == 0) {
           4             return this;
           5         }
           6         char buf[] = new char[count + otherLen];
           7         getChars(0, count, buf, 0);
           8         str.getChars(0, otherLen, buf, count);
           9         return new String(0, count + otherLen, buf);
          10     }
          11 
          12     String(int offset, int count, char value[]) {
          13         this.value = value;
          14         this.offset = offset;
          15         this.count = count;
          16     }
                這段代碼首先創(chuàng)建一個(gè)新的char[],數(shù)組長(zhǎng)度為a.length() + b.length(),然后分別將a和b的內(nèi)容拷貝到新數(shù)組中,最后使用這個(gè)數(shù)組創(chuàng)建一個(gè)新的String對(duì)象。這里我們要特殊注意一下使用的構(gòu)造函數(shù),這個(gè)構(gòu)造函數(shù)只有package訪(fǎng)問(wèn)權(quán)限,它直接使用傳入的char[]作為新生成的String的內(nèi)部字符數(shù)組,而沒(méi)有做任何拷貝保護(hù)。這個(gè)構(gòu)造函數(shù)必須是package級(jí)別的訪(fǎng)問(wèn)權(quán)限,否則你就能用它創(chuàng)建出一個(gè)可變的String對(duì)象(在構(gòu)造完String后修改傳入的char[])。JDK在java.lang中的代碼保證不會(huì)在調(diào)用這一構(gòu)造函數(shù)后再修改傳入的數(shù)組,加上java的安全機(jī)制不允許第三方代碼加入java.lang包(你可以嘗試將自己的類(lèi)放入java.lang包,此類(lèi)將無(wú)法成功加載),所以String的不可變性不會(huì)被破壞。

                整個(gè)過(guò)程我們沒(méi)有創(chuàng)建任何垃圾對(duì)象(我們有言在先,a和b是傳入?yún)?shù),不是連接代碼創(chuàng)建的,所以即使他們變成垃圾我們也不去計(jì)算),所以一切良好!

          方案2:
                使用SB.append(), 這里我使用StringBuilder來(lái)進(jìn)行分析,對(duì)于StringBuffer也是完全一樣的。

          代碼:
                String result = new StringBuilder().append(a).append(b).toString();
                這行代碼明顯比String.concat()方案的代碼復(fù)雜,但它的性能如何呢?讓我們分4步來(lái)分析它new StringBuilder(),append(a),append(b)和toString().
                1)new StringBuilder().
                讓我們來(lái)看看StringBuilder的源代碼:
          1     public StringBuilder() {
          2         super(16);
          3     }
          4 
          5     AbstractStringBuilder(int capacity) {
          6         value = new char[capacity];
          7     }
                它創(chuàng)建了一個(gè)大小為16的char[],目前為止還沒(méi)有創(chuàng)建任何垃圾對(duì)象。
                2)append(a).
                繼續(xù)看源代碼:
           1     public StringBuilder append(String str) {
           2         super.append(str);
           3         return this;
           4     }
           5     public AbstractStringBuilder append(String str) {
           6         if (str == null) str = "null";
           7         int len = str.length();
           8         if (len == 0return this;
           9         int newCount = count + len;
          10         if (newCount > value.length)
          11             expandCapacity(newCount);
          12         str.getChars(0, len, value, count);
          13         count = newCount;
          14         return this;
          15     }
          16     void expandCapacity(int minimumCapacity) {
          17         int newCapacity = (value.length + 1* 2;
          18         if (newCapacity < 0) {
          19             newCapacity = Integer.MAX_VALUE;
          20         } else if (minimumCapacity > newCapacity) {
          21             newCapacity = minimumCapacity;
          22         }
          23         value = Arrays.copyOf(value, newCapacity);
          24     }
                這段代碼首先確保SB的內(nèi)部char[]有足夠的剩余空間,這導(dǎo)致創(chuàng)建了一個(gè)新的大小為34的char[],而之前的大小為16的char[]成為垃圾對(duì)象。標(biāo)記點(diǎn)1,我們創(chuàng)建了第一個(gè)垃圾對(duì)象,大小為16個(gè)char。
                3)append(b).
                相同的邏輯,首先確保內(nèi)部char[]有足夠的剩余空間,這導(dǎo)致創(chuàng)建了一個(gè)新的大小為70的char[],而之前的大小為34的char[]成為垃圾對(duì)象。標(biāo)記點(diǎn)2,我們創(chuàng)建了第二個(gè)垃圾對(duì)象,大小為34個(gè)char。
                 4)toString()
                看源代碼:
           1 public String toString() {
           2         // Create a copy, don't share the array
           3         return new String(value, 0, count);
           4     }
           5     public String(char value[], int offset, int count) {
           6         if (offset < 0) {
           7             throw new StringIndexOutOfBoundsException(offset);
           8         }
           9         if (count < 0) {
          10             throw new StringIndexOutOfBoundsException(count);
          11         }
          12         // Note: offset or count might be near -1>>>1.
          13         if (offset > value.length - count) {
          14             throw new StringIndexOutOfBoundsException(offset + count);
          15         }
          16         this.offset = 0;
          17         this.count = count;
          18         this.value = Arrays.copyOfRange(value, offset, offset+count);
          19     }
                要重點(diǎn)注意一下這次的構(gòu)造函數(shù),它有public訪(fǎng)問(wèn)權(quán)限,所以它必須做拷貝保護(hù),不然就有可能破壞String的不可變性。但這又創(chuàng)建了一個(gè)垃圾對(duì)象。標(biāo)記點(diǎn)3,我們創(chuàng)建了第三個(gè)垃圾對(duì)象,大小為70個(gè)char。

                因此我們一共創(chuàng)建了3個(gè)垃圾對(duì)象,總大小為16+34+70=120個(gè)char! Java使用Unicode-16編碼,這就意味著240byte的垃圾!

                有一件事情能夠改善SB的性能,把代碼改為:
              String result = new StringBuilder(a.length() + b.length()).append(a).append(b).toString();
                自己算一下吧,這次我們只創(chuàng)建了1個(gè)垃圾對(duì)象,大小為17+18=35個(gè)char,還是不怎么樣,不是嗎?

                和String.concat()比起來(lái)SB創(chuàng)建了“許多”垃圾(任何比0大的數(shù)和0比起來(lái)都是無(wú)窮大!),而且相信你也注意到了,SB比String.concat()有更多的方法調(diào)用(棧操作可不是免費(fèi)的)。
             
                進(jìn)一步的分析可以發(fā)現(xiàn)(自己分析吧),當(dāng)你連接少于4個(gè)String時(shí)(不含4),String.concat()要比SB高效的多。

                所以當(dāng)你要連接多于3個(gè)String時(shí)(不含3),我們應(yīng)該使用SB,對(duì)嗎?

                不全對(duì)!

                SB有一個(gè)天生固有的毛病,它使用一個(gè)可以動(dòng)態(tài)增長(zhǎng)的內(nèi)部char[]來(lái)追加新的String,當(dāng)你追加新String且SB達(dá)到了內(nèi)部容量上限時(shí),它就必須擴(kuò)大內(nèi)部緩沖區(qū)。之后SB獲得了一個(gè)更大的char[],而之前使用的char[]則變?yōu)榱死H绻覀兡軌蚓_的告訴SB最終的結(jié)果有多長(zhǎng),它就可以省掉許多由無(wú)謂的增長(zhǎng)產(chǎn)生的垃圾。但想要預(yù)測(cè)最終結(jié)果的長(zhǎng)度并不容易!
             
                與預(yù)測(cè)最終結(jié)果的長(zhǎng)度相比,預(yù)測(cè)要連接String的數(shù)量就顯得容易多了。我們可以先緩存要連接的String,然后在最后那一刻(調(diào)用toString()的時(shí)候)計(jì)算最終結(jié)果的精確長(zhǎng)度,用該長(zhǎng)度創(chuàng)建一個(gè)SB來(lái)連接String,這樣就能節(jié)省掉許多無(wú)謂的中間垃圾char[]。盡管有時(shí)想要精確預(yù)測(cè)要連接的String數(shù)量也是很難的,我們可以效仿SB的做法,使用一個(gè)動(dòng)態(tài)增長(zhǎng)的String[]來(lái)緩存String,因?yàn)镾tring[]要比原來(lái)的char[]小的多(現(xiàn)實(shí)世界中的String普遍多余一個(gè)字符),所以一個(gè)動(dòng)態(tài)增長(zhǎng)的String[]要比動(dòng)態(tài)增長(zhǎng)的char[]便宜的多。接下來(lái)我要介紹的StringBundler就是基于這一原理工作的。

           1     public StringBundler() {
           2         _array = new String[_DEFAULT_ARRAY_CAPACITY]; // _DEFAULT_ARRAY_CAPACITY = 16
           3     }
           4 
           5     public StringBundler(int arrayCapacity) {
           6         if (arrayCapacity <= 0) {
           7             throw new IllegalArgumentException();
           8         }
           9         _array = new String[arrayCapacity];
          10     }
          11 

                第一個(gè)構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè)默認(rèn)數(shù)組大小為16的StringBundler,第二個(gè)構(gòu)造函數(shù)允許你指定一個(gè)初始容量。每當(dāng)你調(diào)用append()時(shí),你并沒(méi)有真正的執(zhí)行String連接操作,而是將該String放置到緩存數(shù)組中。
           1     public StringBundler append(String s) {
           2         if (s == null) {
           3             s = StringPool.NULL;
           4         }
           5         if (_arrayIndex >= _array.length) {
           6             expandCapacity();
           7         }
           8         _array[_arrayIndex++= s;
           9         return this;
          10     }
          11 
                如果你追加的String數(shù)量超過(guò)了緩存數(shù)組容量,內(nèi)部的String[]會(huì)動(dòng)態(tài)增長(zhǎng)。
          1     protected void expandCapacity() {
          2         String[] newArray = new String[_array.length << 1];
          3         System.arraycopy(_array, 0, newArray, 0, _array.length);
          4         _array = newArray;
          5     }
          6 

                擴(kuò)充一個(gè)String[]要比擴(kuò)充char[]便宜的多。因?yàn)镾tring[]比較小,而且增長(zhǎng)的頻度要遠(yuǎn)比原來(lái)的char[]低。
                當(dāng)你完成了全部追加后,調(diào)用toString()來(lái)獲取最終結(jié)果。
           1     public String toString() {
           2         if (_arrayIndex == 0) {
           3             return StringPool.BLANK;
           4         }
           5         String s = null;
           6         if (_arrayIndex <= 3) {
           7             s = _array[0];
           8             for (int i = 1; i < _arrayIndex; i++) {
           9                 s = s.concat(_array[i]);
          10             }
          11         }
          12         else {
          13             int length = 0;
          14             for (int i = 0; i < _arrayIndex; i++) {
          15                 length += _array[i].length();
          16             }
          17             StringBuilder sb = new StringBuilder(length);
          18             for (int i = 0; i < _arrayIndex; i++) {
          19                 sb.append(_array[i]);
          20             }
          21             s = sb.toString();
          22         }
          23         return s;
          24     }
          25 
                如果String的數(shù)量小于4(不含4),使用String.concat()來(lái)連接String,否則首先計(jì)算最終結(jié)果的長(zhǎng)度,再用該長(zhǎng)度來(lái)創(chuàng)建一個(gè)StringBuilder,最后使用這個(gè)StringBuilder來(lái)連接所有String。

                我建議大家如果確定需要連接的String的數(shù)量小于4的,直接使用String.concat()來(lái)連接,雖然StringBundler能夠幫你自動(dòng)處理這一情況,但創(chuàng)建一個(gè)String[]和那些方法調(diào)用都是一些無(wú)謂的開(kāi)銷(xiāo)。
              
                如果大家想進(jìn)一步了解StringBundler,可以查看Liferay的JIRA連接,
                http://support.liferay.com/browse/LPS-6072

                好了,解釋的已經(jīng)夠多了,是時(shí)候看看性能測(cè)試結(jié)果了,這些測(cè)試結(jié)果將向你展示StringBundler能為你帶來(lái)多大的性能提升!

                我們將要比較String.concat(),StringBuffer,StringBuilder,使用默認(rèn)構(gòu)造函數(shù)的StringBundler,使用給定初始化容量構(gòu)造函數(shù)的StringBundler在連接String時(shí)的性能差異。

                具體比較內(nèi)容有兩部分:
          1. 比較在完成相同次數(shù)連接操作情況下,各種連接方式的時(shí)間消耗。
          2. 比較在完成相同次數(shù)連接操作情況下,各種連接方式的垃圾生產(chǎn)量。

                測(cè)試中使用連接String長(zhǎng)度均為17,要連接的String的數(shù)量從72到2,對(duì)每個(gè)連接數(shù)量執(zhí)行100,000次重復(fù)操作。
                對(duì)于1,我只采用連接數(shù)量從40到2時(shí)產(chǎn)生的結(jié)果進(jìn)行比較分析,因?yàn)镴VM的預(yù)熱會(huì)對(duì)前面的結(jié)果產(chǎn)生影響(JIT會(huì)占用大量的CPU時(shí)間)。
                對(duì)于2,我采用全部結(jié)果進(jìn)行比較分析,因?yàn)镴VM的預(yù)熱不會(huì)對(duì)總的垃圾生成數(shù)量產(chǎn)生影響(JIT雖然也會(huì)產(chǎn)生垃圾,但對(duì)于各個(gè)測(cè)試應(yīng)是近似平等的,我只比較差值,所以該影響可以忽略)。

                順便說(shuō)一下,我使用如下JVM參數(shù)來(lái)生成GC日志:
                -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails
                之所以采用SerialGC是為了消除多處理器對(duì)測(cè)試結(jié)果的影響。

                下面的圖片展示各種連接方式間時(shí)間消耗的不同:

                由圖可以看出:
          1. 當(dāng)連接2或3個(gè)String時(shí),String.concat()的性能最好
          2. StringBundler整體上優(yōu)于SB
          3. StringBuilder優(yōu)于StringBuffer(由于節(jié)省了大量的同步操作)
                對(duì)于3,在今后的blog中我還會(huì)更進(jìn)一步的展開(kāi)討論,在我們自己的代碼和JDK的代碼中存在大量相似的情況,許多同步保護(hù)都是不必要的(至少在特定的情況下是不必要的),比如JDK的IO包。如果我們能夠繞過(guò)這些不必要的同步操作,我們就能大幅提高程序性能。

                下面我們來(lái)分析以下GC日志(GC日志并不能100%準(zhǔn)確的告訴你垃圾的數(shù)量,但它可以告訴你一個(gè)大致的趨勢(shì))
          String.concat()  229858963K
          StringBuffer    34608271K
          StringBuilder    34608144K
          StringBundler(默認(rèn)構(gòu)造函數(shù))    21214863K
          StringBundler(明確指定String數(shù)量構(gòu)造函數(shù))
             19562434K

                由統(tǒng)計(jì)數(shù)字可以看出,StringBundler節(jié)省了大量的String垃圾。

                最后我給大家留下4點(diǎn)建議:
          1. 當(dāng)你連接2或3個(gè)String時(shí),使用String.concat()。
          2. 如果你要連接多于3個(gè)String(不含3),并且你能夠精確預(yù)測(cè)出最終結(jié)果的長(zhǎng)度,使用StringBuilder/StringBuffer,并設(shè)定初始化容量。
          3. 如果你要連接多于3個(gè)String(不含3),并且你不能夠精確預(yù)測(cè)出最終結(jié)果的長(zhǎng)度,使用StringBundler。
          4. 如果你使用StringBundler,并且你能預(yù)測(cè)出要連接的String數(shù)量,使用指定初始化容量的構(gòu)造函數(shù)。
                如果你很懶!直接使用StringBundler吧,他在絕大多數(shù)情況下是最佳選擇,在其他情況下雖然他不是最佳選擇,但也能提供足夠的性能保障。

                這里我提供了一個(gè)消除了對(duì)Liferay其他類(lèi)文件依賴(lài)的StringBundler供大家下載使用。不過(guò)還是推薦大家直接學(xué)習(xí)使用Liferay:)
          http://www.aygfsteel.com/Files/ShuyangZhou/StringPerformance/src.zip

          Feedback

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-01-23 09:18 by rox
          好文章,辛苦了!

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-01-23 11:24 by heyang
          標(biāo)記一下。

          # re: String連接性能[未登錄](méi)  回復(fù)  更多評(píng)論   

          2010-01-25 09:19 by 宋針還
          好文章,支持。

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-01-25 15:51 by changedi
          very nice.
          又有可以學(xué)習(xí)的好博客了。
          不過(guò)建議博主:您以后有類(lèi)似ROC曲線(xiàn)之類(lèi)的圖片時(shí),可以放小點(diǎn)就更好了,看起來(lái)一目了然~~~
          very nice.

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-01-25 16:12 by 周舒陽(yáng)
          @changedi
          批評(píng)的有道理,圖片確實(shí)弄大了。已經(jīng)調(diào)小點(diǎn)了。

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-01-26 21:19 by sgz
          講得挺細(xì)的 好!

          # re: String連接性能  回復(fù)  更多評(píng)論   

          2010-11-16 00:05 by XIAOTONG
          寫(xiě)的很好 很需要這種很淳樸但是有理有據(jù)的文章
          謝謝樓主了

          只有注冊(cè)用戶(hù)登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           

          posts - 3, comments - 15, trackbacks - 0, articles - 0

          Copyright © 周舒陽(yáng)

          主站蜘蛛池模板: 铜川市| 古蔺县| 海安县| 呈贡县| 龙游县| 河北省| 蓝山县| 洮南市| 长武县| 黄骅市| 望江县| 玉田县| 集贤县| 丹凤县| 沙雅县| 定边县| 吴堡县| 彝良县| 江川县| 德江县| 辽阳市| 禄丰县| 潢川县| 合肥市| 喀喇沁旗| 永年县| 遵义市| 麻城市| 新昌县| 怀来县| 宁远县| 洛川县| 达拉特旗| 黔东| 内黄县| 大邑县| 德钦县| 澳门| 桂平市| 金阳县| 乌兰浩特市|