from:http://www.lifebackup.cn/timsort-java7.html

2012年09月25日 21:57:48

1. 為什么寫(xiě)這篇文章

這篇文章的根源是在產(chǎn)品中發(fā)現(xiàn)了一個(gè)詭異的bug:只能在產(chǎn)品環(huán)境下重現(xiàn),在我的本地開(kāi)發(fā)環(huán)境無(wú)法重現(xiàn),而雙方的代碼沒(méi)有任何區(qū)別。最后用remote debug的方法找到異常所在:

Exception in thread "main" java.lang.IllegalArgumentException: Comparison 
method violates its general contract!

Google了這個(gè)錯(cuò)誤,是由于Java 7內(nèi)置的新排序算法導(dǎo)致的。這才猛然想起產(chǎn)品的編譯環(huán)境最近升級(jí)到了Java 7。

2. 結(jié)論

在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,內(nèi)部實(shí)現(xiàn)換成了TimSort,其對(duì)對(duì)象間比較的實(shí)現(xiàn)要求更加嚴(yán)格:

Comparator的實(shí)現(xiàn)必須保證以下幾點(diǎn)(出自這兒):

a). sgn(compare(x, y)) == -sgn(compare(y, x)) 
b). (compare(x, y)>0) && (compare(y, z)>0) 意味著 compare(x, z)>0 
c). compare(x, y)==0 意味著對(duì)于任意的z:sgn(compare(x, z))==sgn(compare(y, z)) 均成立

而我們的代碼中,某個(gè)compare()實(shí)現(xiàn)片段是這樣的:

public int compare(ComparatorTest o1, ComparatorTest o2) { 
    return o1.getValue() > o2.getValue() ? 1 : -1; 
}

這就違背了a)原則:假設(shè)X的value為1,Y的value也為1;那么compare(X, Y) ≠ –compare(Y, X) 
PS: TimSort不僅內(nèi)置在各種JDK 7的版本,也存在于Android SDK中(盡管其并沒(méi)有使用JDK 7)。

3. 解決方案

3.1) 更改內(nèi)部實(shí)現(xiàn):例如對(duì)于上個(gè)例子,就需要更改為

public int compare(ComparatorTest o1, ComparatorTest o2) { 
    return o1.getValue() == o2.getValue() ? 0 :  
                (o1.getValue() > o2.getValue() ? 1 : -1); 
}

3.2) Java 7預(yù)留了一個(gè)接口以便于用戶繼續(xù)使用Java 6的排序算法:在啟動(dòng)參數(shù)中(例如eclipse.ini)添加-Djava.util.Arrays.useLegacyMergeSort=true

3.3) 將這個(gè)IllegalArgumentException手動(dòng)捕獲?。ú煌扑])

4. TimSort在Java 7中的實(shí)現(xiàn)

那么為什么Java 7會(huì)將TimSort作為排序的默認(rèn)實(shí)現(xiàn),甚至在某種程度上犧牲它的兼容性(在stackoverflow上有大量的問(wèn)題是關(guān)于這個(gè)新異常的)呢?接下來(lái)我們不妨來(lái)看一看它的實(shí)現(xiàn)。

首先建議大家先讀一下這篇文章以簡(jiǎn)要理解TimSort的思想。

4.1) 如果傳入的Comparator為空,則使用ComparableTimSort的sort實(shí)現(xiàn)。

 image

4.2) 傳入的待排序數(shù)組若小于MIN_MERGE(Java實(shí)現(xiàn)中為32,Python實(shí)現(xiàn)中為64),則

a) 從數(shù)組開(kāi)始處找到一組連接升序或嚴(yán)格降序(找到后翻轉(zhuǎn))的數(shù) 
b) Binary Sort:使用二分查找的方法將后續(xù)的數(shù)插入之前的已排序數(shù)組

image

4.3) 開(kāi)始真正的TimSort過(guò)程:

4.3.1) 選取minRun大小,之后待排序數(shù)組將被分成以minRun大小為區(qū)塊的一塊塊子數(shù)組

a) 如果數(shù)組大小為2的N次冪,則返回16(MIN_MERGE / 2) 
b) 其他情況下,逐位向右位移(即除以2),直到找到介于16和32間的一個(gè)數(shù)

image

4.3.2) 類似于4.2.a找到初始的一組升序數(shù)列 
4.3.3) 若這組區(qū)塊大小小于minRun,則將后續(xù)的數(shù)補(bǔ)足(采用binary sort插入這個(gè)數(shù)組) 
4.3.4) 為后續(xù)merge各區(qū)塊作準(zhǔn)備:記錄當(dāng)前已排序的各區(qū)塊的大小 
4.3.5) 對(duì)當(dāng)前的各區(qū)塊進(jìn)行merge,merge會(huì)滿足以下原則(假設(shè)X,Y,Z為相鄰的三個(gè)區(qū)塊):

a) 只對(duì)相鄰的區(qū)塊merge 
b) 若當(dāng)前區(qū)塊數(shù)僅為2,If X<=Y,將X和Y merge 
b) 若當(dāng)前區(qū)塊數(shù)>=3,If X<=Y+Z,將X和Y merge,直到同時(shí)滿足X>Y+Z和Y>Z

image

4.3.6) 重復(fù)4.3.2 ~ 4.3.5,直到將待排序數(shù)組排序完 
4.3.7) Final Merge:如果此時(shí)還有區(qū)塊未merge,則合并它們

image

5. Demo

這一節(jié)用一個(gè)具體的例子來(lái)演示整個(gè)算法的演進(jìn)過(guò)程:

*注意*:為了演示方便,我將TimSort中的minRun直接設(shè)置為2,否則我不能用很小的數(shù)組演示。。。同時(shí)把MIN_MERGE也改成2(默認(rèn)為32),這樣避免直接進(jìn)入binary sort。

初始數(shù)組為[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14] 
=> 尋找連續(xù)的降序或升序序列 (4.3.2) 
[1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14] 
=> 入棧 (4.3.4) 
當(dāng)前的棧區(qū)塊為[3] 
=> 進(jìn)入merge循環(huán) (4.3.5) 
do not merge因?yàn)闂4笮H為1 
=> 尋找連續(xù)的降序或升序序列 (4.3.2) 
[1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14] 
=> 入棧 (4.3.4) 
當(dāng)前的棧區(qū)塊為[3, 5] 
=> 進(jìn)入merge循環(huán) (4.3.5) 
merge因?yàn)閞unLen[0]<=runLen[1] 
1) gallopRight:尋找run1的第一個(gè)元素應(yīng)當(dāng)插入run0中哪個(gè)位置(”2”應(yīng)當(dāng)插入”1”之后),然后就可以忽略之前run0的元素(都比run1的第一個(gè)元素?。?nbsp;
2) gallopLeft:尋找run0的最后一個(gè)元素應(yīng)當(dāng)插入run1中哪個(gè)位置(”7”應(yīng)當(dāng)插入”8”之前),然后就可以忽略之后run1的元素(都比run0的最后一個(gè)元素大) 
這樣需要排序的元素就僅剩下[5,7] [2,6],然后進(jìn)行mergeLow 
完成之后的結(jié)果: 
[1,2,5,6,7,8,10,12] [4,3,9,11,13,15,16,14] 
=> 入棧 (4.3.4) 
當(dāng)前的棧區(qū)塊為[8] 
退出當(dāng)前merge循環(huán)因?yàn)闂V械膮^(qū)塊僅為1 
=> 尋找連續(xù)的降序或升序序列 (4.3.2) 
[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14] 
=> 入棧 (4.3.4) 
當(dāng)前的棧區(qū)塊大小為[8,2] 
=> 進(jìn)入merge循環(huán) (4.3.5) 
do not merge因?yàn)閞unLen[0]>runLen[1] 
=> 尋找連續(xù)的降序或升序序列 (4.3.2) 
[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14] 
=> 入棧 (4.3.4) 
當(dāng)前的棧區(qū)塊為[8,2,5] 
=> 
do not merege run1與run2因?yàn)椴粷M足runLen[0]<=runLen[1]+runLen[2] 
merge run2與run3因?yàn)閞unLen[1]<=runLen[2] 
1) gallopRight:發(fā)現(xiàn)run1和run2就已經(jīng)排好序 
完成之后的結(jié)果: 
[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
=> 入棧 (4.3.4) 
當(dāng)前入棧的區(qū)塊大小為[8,7] 
退出merge循環(huán)因?yàn)閞unLen[0]>runLen[1] 
=> 尋找連續(xù)的降序或升序序列 (4.3.2) 
最后只剩下[14]這個(gè)元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
=> 入棧 (4.3.4) 
當(dāng)前入棧的區(qū)塊大小為[8,7,1] 
=> 進(jìn)入merge循環(huán) (4.3.5) 
merge因?yàn)閞unLen[0]<=runLen[1]+runLen[2] 
因?yàn)閞unLen[0]>runLen[2],所以將run1和run2先合并。(否則將run0和run1先合并) 
1) gallopRight & 2) gallopLeft 
這樣需要排序的元素剩下[13,15] [14],然后進(jìn)行mergeHigh 
完成之后的結(jié)果: 
[1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16] 當(dāng)前入棧的區(qū)塊為[8,8] 
=> 
繼續(xù)merge因?yàn)閞unLen[0]<=runLen[1] 
1) gallopRight & 2) gallopLeft 
需要排序的元素剩下[5,6,7,8,10,12] [3,4,9,11],然后進(jìn)行mergeHigh 
完成之后的結(jié)果: 
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] 當(dāng)前入棧的區(qū)塊大小為[16] 
=> 
不需要final merge因?yàn)楫?dāng)前棧大小為1 
=> 
結(jié)束

6. 如何重現(xiàn)文章開(kāi)始提到的Exception

這一節(jié)將剝離復(fù)雜的業(yè)務(wù)邏輯,用一個(gè)最簡(jiǎn)單的例子(不修改TimSort.java內(nèi)置的各種參數(shù))重現(xiàn)文章開(kāi)始提到的Exception。因?yàn)楸M管google出來(lái)的結(jié)果中非常多的人提到了這個(gè)Exception及解決方案,但并沒(méi)有人給出一個(gè)可以重現(xiàn)的例子和測(cè)試數(shù)據(jù)。另一方面,我也想從其他角度來(lái)加深對(duì)這個(gè)問(wèn)題的理解。

構(gòu)造測(cè)試數(shù)據(jù)的過(guò)程是個(gè)反人類的過(guò)程:( 大家不要學(xué)我。。

以下是能重現(xiàn)這個(gè)問(wèn)題的代碼:

public class ReproJava7Exception { 
    public static void main(String[] args) { 
        int[] sample = new int[] 
              {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 
                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-2,1,0,-2,0,0,0,0}; 
        List<Integer> list = new ArrayList<Integer>(); 
        for (int i : sample) 
            list.add(i); 
        // use the native TimSort in JDK 7 
        Collections.sort(list, new Comparator<Integer>() { 
            @Override 
            public int compare(Integer o1, Integer o2) { 
                // miss the o1 = o2 case on purpose 
                return o1 > o2 ? 1 : -1; 
            } 
        }); 
    } 
}

7. Sample Code

這篇文章的所有代碼可以到github:https://github.com/Huang-Wei/understanding-timsort-java7下載。

8. References

http://en.wikipedia.org/wiki/Timsort 
http://www.geneffects.com/briarskin/theory/binary/index.html 
http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html#compare%28T,%20T%29 
http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#source

分類:01囈語(yǔ) | 標(biāo)簽:  |

9條評(píng)論

  1. hute說(shuō)道:

    太好了,找了一天.感謝.

  2. hute說(shuō)道:

    重現(xiàn)bug的方法確實(shí)很逆天,作者花了很大力氣吧.

  3. superpippo說(shuō)道:

    @hute 所以說(shuō)這個(gè)過(guò)程是很反人類的。。。

  4. cjnetwork說(shuō)道:

    你好,我測(cè)試了一下你提供的測(cè)試代碼,發(fā)現(xiàn)還是不能復(fù)現(xiàn)這個(gè)異常情況,能否幫忙一下呢。

    以下是我的jdk:
    java version “1.7.0-ea”
    Java(TM) SE Runtime Environment (build 1.7.0-ea-b45)
    Java HotSpot(TM) Client VM (build 14.0-b10, mixed mode, sharing)

  5. superpippo說(shuō)道:

    @cjnetwork 我在jdk 1.7.0_17上用ReproJava7Exception.java能重現(xiàn)這個(gè)問(wèn)題
    我猜測(cè)你不能重現(xiàn)有兩種可能:
    1) 你的這個(gè)jdk版本有點(diǎn)詭異(1.7.0-ea),嘗試升到最1.7的正式版本
    2) 確認(rèn)你在編譯時(shí)候使用的是JDK7,而不是JDK6 – 因?yàn)橛锌赡茉贓clipse或別的IDE中并沒(méi)有設(shè)置正確。你可以寫(xiě)一段switch string的小例子看看有沒(méi)有編譯錯(cuò)誤

  6. ghsau說(shuō)道:

    compare實(shí)現(xiàn)只需要這樣:
    return o1.getValue() – o2.getValue();
    不需要自己判斷.

  7. superpippo說(shuō)道:

    @ghsau 是的