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

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

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

    String連接性能

    Posted on 2010-01-23 00:35 周舒陽 閱讀(2679) 評論(7)  編輯  收藏
          新Blog開張了!

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

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

          Java中的String是一個非常特殊的類,使它特殊的一個主要原因是:String是不可變的(immutable)。
        
          String的不可變性是Java安全機制和線程安全的基石,沒了它Java將變的不堪一擊。

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

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

          首先,需要辟謠,有些人說SB(StringBuffer和StringBuilder)總是比String.concat()有更好的性能。這一說法是不準確的!在特定條件下String.concat()要勝過SB。我們來通過一個例子證明這一點。

    任務:
          連接兩個String,
           String a = "abcdefghijklmnopq"//length=17
           String b = "abcdefghijklmnopqr"//length=18

    說明:
          我們將要來分析一下不同連接方案的垃圾生產(chǎn)情況。討論中我們將忽略由輸入?yún)?shù)引起的垃圾,因為他們不是由連接代碼創(chuàng)建的。另外我們只計算String內(nèi)部的char[],因為除了這個字符數(shù)組String的其它域都非常小,完全可以忽略他們對GC的影響。

    方案1:
          使用String.concat()

    代碼:
           String result = a.concat(b);
          這行代碼簡單到不能再簡單了,不過還是讓我們來看看Sun JDK java.lang.String的源代碼,搞清楚這個調(diào)用究竟是怎樣進行的。
    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)建一個新的char[],數(shù)組長度為a.length() + b.length(),然后分別將a和b的內(nèi)容拷貝到新數(shù)組中,最后使用這個數(shù)組創(chuàng)建一個新的String對象。這里我們要特殊注意一下使用的構(gòu)造函數(shù),這個構(gòu)造函數(shù)只有package訪問權(quán)限,它直接使用傳入的char[]作為新生成的String的內(nèi)部字符數(shù)組,而沒有做任何拷貝保護。這個構(gòu)造函數(shù)必須是package級別的訪問權(quán)限,否則你就能用它創(chuàng)建出一個可變的String對象(在構(gòu)造完String后修改傳入的char[])。JDK在java.lang中的代碼保證不會在調(diào)用這一構(gòu)造函數(shù)后再修改傳入的數(shù)組,加上java的安全機制不允許第三方代碼加入java.lang包(你可以嘗試將自己的類放入java.lang包,此類將無法成功加載),所以String的不可變性不會被破壞。

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

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

    代碼:
          String result = new StringBuilder().append(a).append(b).toString();
          這行代碼明顯比String.concat()方案的代碼復雜,但它的性能如何呢?讓我們分4步來分析它new StringBuilder(),append(a),append(b)和toString().
          1)new StringBuilder().
          讓我們來看看StringBuilder的源代碼:
    1     public StringBuilder() {
    2         super(16);
    3     }
    4 
    5     AbstractStringBuilder(int capacity) {
    6         value = new char[capacity];
    7     }
          它創(chuàng)建了一個大小為16的char[],目前為止還沒有創(chuàng)建任何垃圾對象。
          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[]有足夠的剩余空間,這導致創(chuàng)建了一個新的大小為34的char[],而之前的大小為16的char[]成為垃圾對象。標記點1,我們創(chuàng)建了第一個垃圾對象,大小為16個char。
          3)append(b).
          相同的邏輯,首先確保內(nèi)部char[]有足夠的剩余空間,這導致創(chuàng)建了一個新的大小為70的char[],而之前的大小為34的char[]成為垃圾對象。標記點2,我們創(chuàng)建了第二個垃圾對象,大小為34個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     }
          要重點注意一下這次的構(gòu)造函數(shù),它有public訪問權(quán)限,所以它必須做拷貝保護,不然就有可能破壞String的不可變性。但這又創(chuàng)建了一個垃圾對象。標記點3,我們創(chuàng)建了第三個垃圾對象,大小為70個char。

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

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

          和String.concat()比起來SB創(chuàng)建了“許多”垃圾(任何比0大的數(shù)和0比起來都是無窮大?。蚁嘈拍阋沧⒁獾搅?,SB比String.concat()有更多的方法調(diào)用(棧操作可不是免費的)。
       
          進一步的分析可以發(fā)現(xiàn)(自己分析吧),當你連接少于4個String時(不含4),String.concat()要比SB高效的多。

          所以當你要連接多于3個String時(不含3),我們應該使用SB,對嗎?

          不全對!

          SB有一個天生固有的毛病,它使用一個可以動態(tài)增長的內(nèi)部char[]來追加新的String,當你追加新String且SB達到了內(nèi)部容量上限時,它就必須擴大內(nèi)部緩沖區(qū)。之后SB獲得了一個更大的char[],而之前使用的char[]則變?yōu)榱死?。如果我們能夠精確的告訴SB最終的結(jié)果有多長,它就可以省掉許多由無謂的增長產(chǎn)生的垃圾。但想要預測最終結(jié)果的長度并不容易!
       
          與預測最終結(jié)果的長度相比,預測要連接String的數(shù)量就顯得容易多了。我們可以先緩存要連接的String,然后在最后那一刻(調(diào)用toString()的時候)計算最終結(jié)果的精確長度,用該長度創(chuàng)建一個SB來連接String,這樣就能節(jié)省掉許多無謂的中間垃圾char[]。盡管有時想要精確預測要連接的String數(shù)量也是很難的,我們可以效仿SB的做法,使用一個動態(tài)增長的String[]來緩存String,因為String[]要比原來的char[]小的多(現(xiàn)實世界中的String普遍多余一個字符),所以一個動態(tài)增長的String[]要比動態(tài)增長的char[]便宜的多。接下來我要介紹的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òu)造函數(shù)會創(chuàng)建一個默認數(shù)組大小為16的StringBundler,第二個構(gòu)造函數(shù)允許你指定一個初始容量。每當你調(diào)用append()時,你并沒有真正的執(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ù)量超過了緩存數(shù)組容量,內(nèi)部的String[]會動態(tài)增長。
    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 

          擴充一個String[]要比擴充char[]便宜的多。因為String[]比較小,而且增長的頻度要遠比原來的char[]低。
          當你完成了全部追加后,調(diào)用toString()來獲取最終結(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()來連接String,否則首先計算最終結(jié)果的長度,再用該長度來創(chuàng)建一個StringBuilder,最后使用這個StringBuilder來連接所有String。

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

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

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

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

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

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

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

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

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

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

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

          這里我提供了一個消除了對Liferay其他類文件依賴的StringBundler供大家下載使用。不過還是推薦大家直接學習使用Liferay:)
    http://www.tkk7.com/Files/ShuyangZhou/StringPerformance/src.zip

    Feedback

    # re: String連接性能  回復  更多評論   

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

    # re: String連接性能  回復  更多評論   

    2010-01-23 11:24 by heyang
    標記一下。

    # re: String連接性能[未登錄]  回復  更多評論   

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

    # re: String連接性能  回復  更多評論   

    2010-01-25 15:51 by changedi
    very nice.
    又有可以學習的好博客了。
    不過建議博主:您以后有類似ROC曲線之類的圖片時,可以放小點就更好了,看起來一目了然~~~
    very nice.

    # re: String連接性能  回復  更多評論   

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

    # re: String連接性能  回復  更多評論   

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

    # re: String連接性能  回復  更多評論   

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

    只有注冊用戶登錄后才能發(fā)表評論。


    網(wǎng)站導航:
     

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

    Copyright © 周舒陽

    主站蜘蛛池模板: 亚洲三级视频在线| 天天影院成人免费观看| 亚洲AV综合色区无码二区爱AV| 国产一区二区三区免费在线观看| 人人揉揉香蕉大免费不卡| 亚洲大码熟女在线观看| 亚洲视频日韩视频| 国产亚洲色婷婷久久99精品| 免费国产a国产片高清网站| 男女做羞羞的事视频免费观看无遮挡| 中文字幕免费观看视频| 在线精品自拍亚洲第一区| 亚洲va久久久久| 亚洲国产成人精品久久| 亚洲伊人tv综合网色| 亚洲人精品午夜射精日韩 | 亚洲高清专区日韩精品| 亚洲高清视频一视频二视频三| 无人影院手机版在线观看免费| 最近中文字幕大全中文字幕免费 | 亚洲综合国产精品第一页| 日韩免费a级在线观看| 成人毛片免费播放| 免费电影在线观看网站| 免费成人福利视频| 在免费jizzjizz在线播| 67194国产精品免费观看| 免费人成在线观看网站品爱网 | 亚洲免费人成在线视频观看| 亚洲精品无码AV中文字幕电影网站 | 免费h成人黄漫画嘿咻破解版| 日本一道本高清免费| 国产精品酒店视频免费看| 日本不卡视频免费| 国产精品va无码免费麻豆| 国产成人综合久久精品免费| 国产禁女女网站免费看| 免费一级毛片在播放视频| 亚洲成a人片在线观看老师| 亚洲高清最新av网站| 久久乐国产精品亚洲综合|