2014-02-23 118 views
11

爲了練習Java 8流,我嘗試將下面的嵌套循環轉換爲Java 8流API。它計算^ B的最大數字和(A,B < 100),並採取〜0.135s我的酷睿i5 760Java 8與流和性能的嵌套循環

public static int digitSum(BigInteger x) 
{ 
    int sum = 0; 
    for(char c: x.toString().toCharArray()) {sum+=Integer.valueOf(c+"");} 
    return sum; 
} 

@Test public void solve() 
    { 
     int max = 0; 
     for(int i=1;i<100;i++) 
      for(int j=1;j<100;j++) 
       max = Math.max(max,digitSum(BigInteger.valueOf(i).pow(j))); 
     System.out.println(max); 
    } 

我的解決方案,我預計會更快,因爲的paralellism居然拿了快閃(0.19s沒有parallel()):

int max = IntStream.range(1,100).parallel() 
      .map(i -> IntStream.range(1, 100) 
      .map(j->digitSum(BigInteger.valueOf(i).pow(j))) 
      .max().getAsInt()).max().getAsInt(); 

我的問題

  • 沒有我做轉換權或是否有更好的辦法ŧ o將嵌套循環轉換爲流計算?
  • 爲什麼流的變體比舊的慢?
  • 爲什麼parallel()語句實際上將時間從0.19s增加到0.25s?

我知道microbenchmarks是脆弱的,並行度只是值得的大問題,但對於一個CPU,即使0.1s是永恆的,對吧?

更新

我在Eclipse開普勒JUnit的4個構架測量(它示出了採取用於執行測試的時間)。

我的結果爲A,B < 1000而不是100:

  • 傳統循環186s
  • 順序流193S
  • 並行流55S

更新2 更換sum+=Integer.valueOf(c+"");sum+= c - '0';(感謝彼得!)削減了整整10秒並行方法,使其達到45秒。沒想到會有如此大的性能影響!另外,降低CPU核心數(在我的情況下爲4)的並行性並沒有太大的作用,因爲它只將時間減少到44.8s(是的,它增加了a和b = 0,但我認爲這贏得了對性能影響不大):

int max = IntStream.range(0, 3).parallel(). 
      .map(m -> IntStream.range(0,250) 
      .map(i -> IntStream.range(1, 1000) 
      .map(j->.digitSum(BigInteger.valueOf(250*m+i).pow(j))) 
      .max().getAsInt()).max().getAsInt()).max().getAsInt(); 
+2

你如何衡量?正如你指出的那樣,如果沒有適當的照顧,微基準的結果可能會產生誤導。 – assylias

+3

我會用'sum + = c - '0'來代替'sum + = Integer.valueOf(c +「」);'',因爲這樣會快得多。 –

+2

FWIW你可以使用'CharSequence.chars()'方法用'stream'代替'digitSum'中的循環。它避免了分配char數組。 –

回答

22

我已經根據您的代碼創建了一個快速而髒的微型基準測試。結果是:

循環:3192
拉姆達:3140
拉姆達並行:868

所以循環和lambda是等價的,平行流顯著提高性能。由於您的基準測試方法,我懷疑您的結果不可靠。

public static void main(String[] args) { 
    int sum = 0; 

    //warmup 
    for (int i = 0; i < 100; i++) { 
     solve(); 
     solveLambda(); 
     solveLambdaParallel(); 
    } 

    { 
     long start = System.nanoTime(); 
     for (int i = 0; i < 100; i++) { 
      sum += solve(); 
     } 
     long end = System.nanoTime(); 
     System.out.println("loop: " + (end - start)/1_000_000); 
    } 
    { 
     long start = System.nanoTime(); 
     for (int i = 0; i < 100; i++) { 
      sum += solveLambda(); 
     } 
     long end = System.nanoTime(); 
     System.out.println("lambda: " + (end - start)/1_000_000); 
    } 
    { 
     long start = System.nanoTime(); 
     for (int i = 0; i < 100; i++) { 
      sum += solveLambdaParallel(); 
     } 
     long end = System.nanoTime(); 
     System.out.println("lambda parallel : " + (end - start)/1_000_000); 
    } 
    System.out.println(sum); 
} 

public static int digitSum(BigInteger x) { 
    int sum = 0; 
    for (char c : x.toString().toCharArray()) { 
     sum += Integer.valueOf(c + ""); 
    } 
    return sum; 
} 

public static int solve() { 
    int max = 0; 
    for (int i = 1; i < 100; i++) { 
     for (int j = 1; j < 100; j++) { 
      max = Math.max(max, digitSum(BigInteger.valueOf(i).pow(j))); 
     } 
    } 
    return max; 
} 

public static int solveLambda() { 
    return IntStream.range(1, 100) 
      .map(i -> IntStream.range(1, 100).map(j -> digitSum(BigInteger.valueOf(i).pow(j))).max().getAsInt()) 
      .max().getAsInt(); 
} 

public static int solveLambdaParallel() { 
    return IntStream.range(1, 100) 
      .parallel() 
      .map(i -> IntStream.range(1, 100).map(j -> digitSum(BigInteger.valueOf(i).pow(j))).max().getAsInt()) 
      .max().getAsInt(); 
} 

我還與江鈴控股比手動測試更可靠的運行。結果與上述一致(每次通話的微秒數):

Benchmark        Mode Mean  Units 
c.a.p.SO21968918.solve     avgt 32367.592 us/op 
c.a.p.SO21968918.solveLambda    avgt 31423.123 us/op 
c.a.p.SO21968918.solveLambdaParallel  avgt 8125.600 us/op 
+3

如果按相反的順序運行測試,我會很感興趣的。 –

+2

@PeterLawrey相同的結果(lambda parallel:836,lambda:3124,loop:3184) – assylias

+0

有趣的結果!也許我的也是由於英特爾Turbo Boost(如果只使用一個內核就自動超頻)?然而,我不太確定Junit是否真的在計時中不可靠,因爲我重複了幾次,總是得到類似的結果。 –

3

您遇到的問題是您正在尋找次優代碼。當你有可能被大量優化的代碼時,你非常依賴JVM是否足夠聰明來優化你的代碼。循環周圍時間更長,更好理解。

你的循環代碼有一個很大的區別,就是你的工作集非常小。您一次只考慮一個最大數字金額。這意味着代碼緩存友好,並且您有非常短暫的對象。在stream()的情況下,你正在構建集合,在任何時候工作集中都有更多的集合,使用更多的緩存和更多的開銷。我希望你的GC時間也更長和/或更頻繁。

爲什麼流的變體比舊的慢很多?

從Java開發之前就已經有很多循環被優化了。它們可以非常有效地映射到硬件。數據流相當新,並沒有經過嚴格優化。

爲什麼parallel()語句實際上將時間從0.19s增加到0.25s?

很有可能你在共享資源上有瓶頸。你創造了相當多的垃圾,但這通常是相當併發的。使用更多的線程,只能保證你會有更多的開銷,但不能確保你可以利用你擁有的額外CPU能力。

+0

嗯,但看着我的代碼我沒有看到任何共享資源,我只是使用Java庫,或者我忽略了什麼? –

+1

@ kirdie您有共享的硬件資源。例如你的L3高速緩存,可能是L1/L2高速緩存,你的主內存和垃圾收集器可能會起作用。 –