簡體   English   中英

增強的循環性能比傳統的索引查找更差?

[英]Enhanced for loop performance worse than traditional indexed lookup?

我剛剛看到這個看似無害的評論 ,對ArrayList和原始String數組進行基准測試。 它來自幾年前,但OP寫道

我注意到使用String s:stringsList比使用舊式for循環訪問列表慢約50%。 去搞清楚...

沒有人在原帖中評論它,測試看起來有點可疑(太短不准確),但是當我讀到它時,我差點從椅子上掉下來。 我從來沒有對一個“傳統”的循環增強循環進行基准測試,但我目前正在開發一個項目,它使用增強循環對ArrayList實例進行數億次迭代,所以這是我關注的問題。

我打算做一些基准測試並在此發表我的發現,但這顯然是我的一個大問題。 我可以在網上找到關於相對性能的寶貴的小信息,除了一些隨便提到的ArrayLists的增強循環在Android下運行速度慢得多

有沒有人經歷過這個? 這種性能差距是否仍然存在? 我會在這里發布我的發現,但讀到它時非常驚訝。 我懷疑如果這個性能差距確實存在,它已經在更現代的VM中得到修復,但我想我現在必須做一些測試並確認。

更新:我對我的代碼進行了一些更改,但是已經懷疑其他人已經指出的內容:確保增強的for循環速度較慢,但​​是在非常簡單的緊密循環之外,成本應該是成本的一​​小部分。循環的邏輯。 在我的情況下,即使我使用增強型循環迭代非常大的字符串列表,我在循環中的邏輯也足夠復雜,甚至在切換到基於索引的循環之后我甚至無法測量差異。

TL; DR:增強循環確實比傳統的基於索引的循環慢於arraylist; 但對於大多數應用來說,差異應該可以忽略不計。

您遇到的問題是使用Iterator比使用直接查找要慢。 在我的機器上,每次迭代的差異大約為0.13 ns。 使用數組代替每次迭代節省大約0.15 ns。 在99%的情況下,這應該是微不足道的。

public static void main(String... args) {
    int testLength = 100 * 1000 * 1000;
    String[] stringArray = new String[testLength];
    Arrays.fill(stringArray, "a");
    List<String> stringList = new ArrayList<String>(Arrays.asList(stringArray));
    {
        long start = System.nanoTime();
        long total = 0;
        for (String str : stringArray) {
            total += str.length();
        }
        System.out.printf("The for each Array loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
    {
        long start = System.nanoTime();
        long total = 0;
        for (int i = 0, stringListSize = stringList.size(); i < stringListSize; i++) {
            String str = stringList.get(i);
            total += str.length();
        }
        System.out.printf("The for/get List loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
    {
        long start = System.nanoTime();
        long total = 0;
        for (String str : stringList) {
            total += str.length();
        }
        System.out.printf("The for each List loop time was %.2f ns total=%d%n", (double) (System.nanoTime() - start) / testLength, total);
    }
}

當以10億個條目運行條目打印時(使用Java 6更新26)

The for each Array loop time was 0.76 ns total=1000000000
The for/get List loop time was 0.91 ns total=1000000000
The for each List loop time was 1.04 ns total=1000000000

以10億個條目運行時打印條目(使用OpenJDK 7)

The for each Array loop time was 0.76 ns total=1000000000
The for/get List loop time was 0.91 ns total=1000000000
The for each List loop time was 1.04 ns total=1000000000

即完全相同。 ;)

每個聲稱X在JVM上比Y慢,並沒有解決本文中提到的所有問題,而它的第二部分則傳播了對典型JVM性能的擔憂和謊言。 這適用於原始問題所引用的評論以及GravityBringer的答案。 我很抱歉這么粗魯,但除非你使用適當的微基准測試技術,否則你的基准測試會產生非常嚴重偏斜的隨機數。

如果您對更多解釋感興趣,請告訴我。 雖然這些都在我提到的文章中。

GravityBringer的數字似乎不正確,因為我知道ArrayList.get()與VM優化后的原始數組訪問速度一樣快。

我在我的機器-server模式下運行了兩次GravityBringer測試

50574847
43872295
30494292
30787885
(2nd round)
33865894
32939945
33362063
33165376

這種測試的瓶頸實際上是內存讀/寫。 從數字來看,整個2個陣列都在我的L2緩存中。 如果我們減小大小以適應L1緩存,或者如果我們將大小增加到L2緩存之外,我們將看到10倍的吞吐量差異。

ArrayList的迭代器使用單個int計數器。 即使VM沒有將它放入寄存器(循環體太復雜),至少它將在L1緩存中,因此r / w基本上是免費的。

最終的答案當然是在您的特定環境中測試您的特定程序。

雖然每當提出基准問題時玩不可知論者都沒有幫助。

ArrayLists的情況變得更糟。 在運行Java 6.26的計算機上,存在四倍的差異。 有趣的是(也許在邏輯上相當),原始數組沒有區別。 我運行了以下測試:

    int testSize = 5000000;

    ArrayList<Double> list = new ArrayList<Double>();
    Double[] arr = new Double[testSize];

    //set up the data - make sure data doesn't have patterns
    //or anything compiler could somehow optimize
    for (int i=0;i<testSize; i++)
    {
        double someNumber = Math.random();
        list.add(someNumber);
        arr[i] = someNumber;
    }

    //ArrayList foreach
    long time = System.nanoTime();
    double total1 = 0;
    for (Double k: list)
    {
        total1 += k;
    }
    System.out.println (System.nanoTime()-time);

    //ArrayList get() method
    time = System.nanoTime();
    double total2 = 0;
    for (int i=0;i<testSize;i++)
    {
        total2 += list.get(i);  
    }
    System.out.println (System.nanoTime()-time);        

    //array foreach
    time = System.nanoTime();
    double total3 = 0;
    for (Double k: arr)
    {
        total3 += k;
    }
    System.out.println (System.nanoTime()-time);

    //array indexing
    time = System.nanoTime();
    double total4 = 0;
    for (int i=0;i<testSize;i++)
    {
        total4 += arr[i];
    }
    System.out.println (System.nanoTime()-time);

    //would be strange if different values were produced,
    //but no, all these are the same, of course
    System.out.println (total1);
    System.out.println (total2);        
    System.out.println (total3);
    System.out.println (total4);

循環中的算法是防止JIT編譯器可能優化掉一些代碼。 算術對性能的影響很小,因為運行時由ArrayList訪問控制。

運行時間(以納秒為單位):

ArrayList foreach:248,351,782

ArrayList get():60,657,907

陣列foreach:27,381,576

數組直接索引:27,468,091

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM