3個1次操作的for循環和1個for循環3次操作效率是一樣的嗎?

/****************************************************************/
/*
提出這個問題是因為我寫了3個lambda表達式,當然是有辦法將代碼全寫在1個表達式里,或者放在一個鏈操作里的。但是不太美觀,不容易閱讀。
而且中間還有一些奇奇怪怪的操作。
所以如果效率差不多,我就想著不改啦~
*/
/*****************************************************************/

//第一種情況
for(int i=0;i&<100;i++) { //opr 1 } for(int i=0;i&<100;i++) { //opr 2 } for(int i=0;i&<100;i++) { //opr 3 } //第二種情況 for(int i=0;i&<100;i++) { //opr 1 //opr 2 //opr 3 } //對於Java語言來說,以上2種情況效率有區別嗎?


我的工作就是在國內最棒的CPU設計團隊,從事這方面的性能驗證工作。

針對於for的性能,cpu使用的是branch predictor 中的loop模塊。這個模塊會根據歷史信息,去預測for循環未來的行為。

現在從幾個緯度去評論以上兩種寫法的好壞。

執行性能:

一個for循環比三個for循環,cpu在第一次訓練過程中,會少出現兩次misp,但是,在以後再次遇到這個for語句的預測過程中兩種寫法是一樣的。總結:一個for循環會節省幾十個時鐘周期,具體數字和cpu架構和晶元頻率有關,大約5ns左右。

佔用CPU資源:

一個for循環會比三個for循環更少。這個資源只是預測中一個子模塊的資源,並不是執行單元的資源。雖然,節省三分之一,但是,並不是很多。

Java代碼可讀性:

代碼的可讀性和可維護性,對於軟體工程師很重要的。以上三個for循環,所多佔用的CPU資源和耽誤的時間,對於整個程序來講,實在微不足道。對於初學者,寫出整潔的代碼是十分重要的。


Java代碼和CPU執行的代碼之間存在「語義溝」。 -- 《Effective Java》,Item 55

基本上你寫的Java代碼,和編譯器看到的代碼,和最終機器執行的指令,可能是3個完全不同的東西。所以不要猜,直接基準測試。

先說結論:單個多操作的循環性能和多個但操作循環差不多。大多數情況前者會好一點點。不過可以忽略(理論原因見 @maxuyan 的回答) 。極端場景下會因為JVM的優化出現多個循環更快的情況,不過這屬於極個別情況。

但是,從工程的角度還是還是不要拆開來寫。代碼的可讀性,比這麼一點點的性能優化重要多了。

考慮下面三種情況:

1. 循環里直接執行簡單操作

public static long oneLoop(int times) {
long start = System.nanoTime();
for (int i = 0, x = 0; i &< times; i++) { x++; x++; x++; } long end = System.nanoTime(); return end - start; } // 對應測試拆分成3個for循環

2. 循環里調用簡單方法

private static int plusOne(int i) {
return ++i;
}
public static long oneLoopCallMethod(int times) {
long start = System.nanoTime();
for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); x = plusOne(x); x = plusOne(x); } long end = System.nanoTime(); return end - start; } // 對應測試拆分成3個for循環

3. 循環里模擬比較複雜的調用

private static final Random R = new Random();
private static final char[] LETTERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
// 方法1
private static String randomWord() {
int length = R.nextInt(10);
char[] chars = new char[length];
for (int i = 0; i &< length; i++) { chars[i] = LETTERS[R.nextInt(LETTERS.length)]; } return new String(chars); } // 方法2 private static String randomUpperWord() { return randomWord().toUpperCase(); } // 方法3 private static int wordLength() { return randomWord().length(); } // 測試 public static long oneLoopThreeOperation(int times) { long start = System.nanoTime(); for (int i = 0; i &< times; i++) { randomWord(); randomUpperWord(); wordLength(); } long end = System.nanoTime(); return end - start; } // 對應測試拆分成3個for循環

測試框架很簡單,單元測試內循環10000次,測總時間(納秒)。然後每個單元測試重複100次,丟棄前10次預熱結果,計平均時間。

long[] result = new long[6];
int loops= 100;
for (int i = 0; i &< loops; i++) { if (i &>= 10) { // 丟棄前10次預熱
result[0] += oneLoop(10000);
result[1] += threeLoop(10000);
result[2] += oneLoopCallMethod(10000);
result[3] += threeLoopCallMethod(10000);
result[4] += oneLoopThreeOperation(10000);
result[5] += threeLoopThreeOperation(10000);
}
}
// 列印以上時間/90

結果有點出乎意料,

  1. 簡單自增操作,單個for比三個for快一點。
  2. 調用同一個方法,單個for比三個for慢了接近一倍。
  3. 接下來複雜場景下的結果恢復正常,單個循環還是要好一些。

One loop: 10838
Three loop: 12905
One loop call method: 52459
Three loop call method: 29699 // WTF
One loop call expensive method: 2617674
Three loop call expensive method: 2740937

為了搞清楚為什麼第二種方法為什麼用三個for循環反而快,用 -XX:+PrintCompilation 參數列印了TIJ的編譯情況。但也看不出有什麼問題。只顯示了兩種方法都內聯了plusOne()方法。(THX @神楽坂茉奈 )

真實的原因,因為用一個循環,只會內聯plusOne()方法,

for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); x = plusOne(x); x = plusOne(x); }

但如果是相同的三次循環,TIJ有可能直接把整個循環編譯了。後面兩次就會節省時間。

for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); } for (int i = 0, x = 0; i &< times; i++) { // TIJ編譯了整個循環 x = plusOne(x); } for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); }


我用C++試了試,山寨了 @胖胖 的代碼,環境是win10, i7-6700k @ 4.5G, 16G RAM @ 3000MHz,實際上不管是單個CPU核心還是RAM都跑不滿,明顯循環太多,訪問速度瓶頸了。100次計時波動非常大,我又跑了1000次,還是波動很大。我試了幾遍,大小關係各不相同,參考意義不大。這部分結果在帖子最後的GitHub項目的readme文本裡面有,但沒什麼代表性意義,這裡不貼了。

後來又跑了10億次累計,結果一次循環內計算三次(integrate)相比分三次循環(separate)還微弱反超了.......加inline以後沒看出區別。

跑10億(1E9)次計數:

Input loops to run:
1000000000
integrated_for: 14.8361
separate_for: 13.3647
integrated_function: 14.9002
separate_function: 13.6677

然後把遞增函數調用改成inline,手工清除生成的exe,重新編譯,跑10億次計數:

Input loops to run:
1000000000
integrated_for: 14.8307
separate_for: 13.3544
integrated_function: 14.8999
separate_function: 13.6537

關掉編譯器的所有優化,跑1百萬(1E6)次計數:

Input loops to run:
1000000
integrated_for: 36.1264
separate_for: 51.0167
integrated_function: 92.7392
separate_function: 94.8164

關掉編譯器的所有優化,給遞增函數加上inline,跑1百萬次計數:

Input loops to run:
1000000
integrated_for: 36.1399
separate_for: 51.0258
integrated_function: 92.7766
separate_function: 94.8581

關掉優化後整體慢了將近1000倍? 不過關掉優化後分開調用三次循環就顯得慢了很多。但對函數分別調用三遍循環跟合在一起循環仍然區別不大。加inline以後仍然沒看出區別。

源代碼: https://github.com/sylvannus/Loop-test/blob/master/for%20loop%20test/for%20loop%20test.cpp


3*O(1) 和 O(3) 啊…

噗。想起來這個梗就想笑。。

其實也要看情況。一般來說compose可能性能更好一點。

class Test{

public long one(){
long start = System.nanoTime();
int[] arr = new int[100000];
long total = 0;
for(int i=0;i&

count基本都是980次左右...有點暈,還不是絕對的,jni的原因么


如果在工程中。主要還是考慮可讀性吧。


一些基於性能的討論要拿profiling結果說話呀

題主不如跑個性能測試


完全取決於你幹什麼。比如前者可能破壞數據局域性影響性能,後者可能三件事互相攪和緩衝區影響性能。


分成三個循環編譯器容易向量化吧,那效率就完全不是一個檔次的了

JAVA不清楚,反正C++裡面我已經完全慣用map或者其他什麼一路到底了


山寨了@胖胖 的代碼,這裡做了一點基本的測試

public class Test {
private static long oneFor(int times) {
long start = System.nanoTime();
for (int i = 0, x = 0; i &< times; i++) { x++; x++; x++; } long end = System.nanoTime(); return end - start; } private static long threeFor(int times) { long start = System.nanoTime(); for (int i = 0, x = 0; i &< times; i++) { x++; } for (int i = 0, x = 0; i &< times; i++) { x++; } for (int i = 0, x = 0; i &< times; i++) { x++; } long end = System.nanoTime(); return end - start; } private static int plusOne(int i) { return ++i; } private static long oneForMethod(int times) { long start = System.nanoTime(); for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); x = plusOne(x); x = plusOne(x); } long end = System.nanoTime(); return end - start; } private static long threeForMethod(int times) { long start = System.nanoTime(); for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); } for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); } for (int i = 0, x = 0; i &< times; i++) { x = plusOne(x); } long end = System.nanoTime(); return end - start; } private static List& randomList(int size) {
Random r = new Random();
List& list = new ArrayList&<&>();
for (int i = 0; i &< size; i++) { list.add(r.nextInt(size)); } return list; } private static void operationExpensive(int size) { Collections.sort(randomList(size)); } private static long oneForExpensiveMethod(int times) { long start = System.nanoTime(); for (int i = 0; i &< times; i++) { operationExpensive(1000); operationExpensive(1000); operationExpensive(1000); } long end = System.nanoTime(); return end - start; } private static long threeForExpensiveMethod(int times) { long start = System.nanoTime(); for (int i = 0; i &< times; i++) { operationExpensive(1000); } for (int i = 0; i &< times; i++) { operationExpensive(1000); } for (int i = 0; i &< times; i++) { operationExpensive(1000); } long end = System.nanoTime(); return end - start; } public static void main(String[] args) { if(args.length != 2){ System.out.println("Usage: Test & &");
return;
}

int times = Integer.parseInt(args[1]);
if (args[0].equals("One")) {
System.out.println("One");
System.out.println(oneFor(times));
System.out.println(oneForMethod(times));
System.out.println(oneForExpensiveMethod(times));
} else {
System.out.println("Three");
System.out.println(threeFor(times));
System.out.println(threeForMethod(times));
System.out.println(threeForExpensiveMethod(times));
}

}
}

通過分別對比不同的循環次數有了下面的結果(為了避免JVM造成的影響,測試的時候每次都開了一個新的進程。測試環境:Windows 10 Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode) CPU:i7 6700K@4.5GHz 內存:32G。單線程運行):

縱坐標是耗時,橫坐標是迭代次數。似乎是出現了一些比較迷的事情。

於是決定分析一下編譯後的代碼:

Decoding compiled method 0x00000000029b4a10:
Code:
Argument 0 is unknown.RIP: 0x29b4b60 Code size: 0x00000130
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x0000000025290910} "plusOne" "(I)I" in "Test"
# parm0: rdx = int
# [sp+0x40] (sp of caller)
0x00000000029b4b60: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000029b4b67: push rbp
0x00000000029b4b68: sub rsp,30h
0x00000000029b4b6c: mov rax,25291550h ; {metadata(method data for {method} {0x0000000025290910} "plusOne" "(I)I" in "Test")}
0x00000000029b4b76: mov esi,dword ptr [rax+0dch]
0x00000000029b4b7c: add esi,8h
0x00000000029b4b7f: mov dword ptr [rax+0dch],esi
0x00000000029b4b85: mov rax,25290908h ; {metadata({method} {0x0000000025290910} "plusOne" "(I)I" in "Test")}
0x00000000029b4b8f: and esi,1ff8h
0x00000000029b4b95: cmp esi,0h
0x00000000029b4b98: je 29b4bafh ;*iinc
; - Test::plusOne@0 (line 35)

0x00000000029b4b9e: inc edx
0x00000000029b4ba0: mov rax,rdx
0x00000000029b4ba3: add rsp,30h
0x00000000029b4ba7: pop rbp
0x00000000029b4ba8: test dword ptr [0ad0100h],eax
; {poll_return}
0x00000000029b4bae: ret
0x00000000029b4baf: mov qword ptr [rsp+8h],rax
0x00000000029b4bb4: mov qword ptr [rsp],0ffffffffffffffffh
0x00000000029b4bbc: call 299fba0h ; OopMap{off=97}
;*synchronization entry
; - Test::plusOne@-1 (line 35)
; {runtime_call}
0x00000000029b4bc1: jmp 29b4b9eh
0x00000000029b4bc3: nop
0x00000000029b4bc4: nop
0x00000000029b4bc5: mov rax,qword ptr [r15+2a8h]
0x00000000029b4bcc: mov r10,0h
0x00000000029b4bd6: mov qword ptr [r15+2a8h],r10
0x00000000029b4bdd: mov r10,0h
0x00000000029b4be7: mov qword ptr [r15+2b0h],r10
0x00000000029b4bee: add rsp,30h
0x00000000029b4bf2: pop rbp
0x00000000029b4bf3: jmp 290d120h ; {runtime_call}
0x00000000029b4bf8: hlt
0x00000000029b4bf9: hlt
0x00000000029b4bfa: hlt
0x00000000029b4bfb: hlt
0x00000000029b4bfc: hlt
0x00000000029b4bfd: hlt
0x00000000029b4bfe: hlt
0x00000000029b4bff: hlt
[Exception Handler]
[Stub Code]
0x00000000029b4c00: call 299c5a0h ; {no_reloc}
0x00000000029b4c05: mov qword ptr [rsp+0ffffffffffffffd8h],rsp
0x00000000029b4c0a: sub rsp,80h
0x00000000029b4c11: mov qword ptr [rsp+78h],rax
0x00000000029b4c16: mov qword ptr [rsp+70h],rcx
0x00000000029b4c1b: mov qword ptr [rsp+68h],rdx
0x00000000029b4c20: mov qword ptr [rsp+60h],rbx
0x00000000029b4c25: mov qword ptr [rsp+50h],rbp
0x00000000029b4c2a: mov qword ptr [rsp+48h],rsi
0x00000000029b4c2f: mov qword ptr [rsp+40h],rdi
0x00000000029b4c34: mov qword ptr [rsp+38h],r8
0x00000000029b4c39: mov qword ptr [rsp+30h],r9
0x00000000029b4c3e: mov qword ptr [rsp+28h],r10
0x00000000029b4c43: mov qword ptr [rsp+20h],r11
0x00000000029b4c48: mov qword ptr [rsp+18h],r12
0x00000000029b4c4d: mov qword ptr [rsp+10h],r13
0x00000000029b4c52: mov qword ptr [rsp+8h],r14
0x00000000029b4c57: mov qword ptr [rsp],r15
0x00000000029b4c5b: mov rcx,601ef1c0h ; {external_word}
0x00000000029b4c65: mov rdx,29b4c05h ; {internal_word}
0x00000000029b4c6f: mov r8,rsp
0x00000000029b4c72: and rsp,0fffffffffffffff0h
0x00000000029b4c76: call 5feaed50h ; {runtime_call}
0x00000000029b4c7b: hlt
[Deopt Handler Code]
0x00000000029b4c7c: mov r10,29b4c7ch ; {section_word}
0x00000000029b4c86: push r10
0x00000000029b4c88: jmp 28e7600h ; {runtime_call}
0x00000000029b4c8d: hlt
0x00000000029b4c8e: hlt
0x00000000029b4c8f: hlt

基本上都是很正常的代碼了。突然意識到,曲線中有兩個拐點似乎可能代表發生了兩個事件,一個是進行了動態編譯,一個是進行了動態內聯

先碼這麼多,過兩天繼續,要趕火車去了


分散開寫更可能對緩存不友好(蒙的


反正這種寫法在實踐中寫出來就是要被罵的


不管效率如何,第一種寫法強迫症患者接受不了啊


這個問題沒有具體的代碼是沒法回答的,我只能說性能有時候能差好幾個數量級


想說 第一種 跟第二張的結果不是不一樣的嗎

1

1

1

.

.

2

2

2

.

.

3

3

3

.

.

1

2

3

1

2

3

1

2

3

.

.


個人覺得運行一個3次的循環很節省運行時間 多一個循環就多一個循環的標記語句


如果是java,真不要考慮效率問題,簡潔與邏輯才是重點。這點兒渣渣效率留給語言本身和搞cpu的去解決吧。前面有人說到c++執行1億遍只差60ms!!!


不一樣,第二種好,但無所謂


要是gpu上搞這式子多好


你這兩個片段執行的結果都不一樣,順序不一樣啊。

lambda都用了,就不要用for了,stream更java 8


新手程序員,個人看法

3次for,3次變數定義,3*100次變數自增 完成了和1次定義 100次變數自增相同的工作(假設這些操作都是離線的)

編譯器大概、可能、應該……不可能對這個做優化的


推薦閱讀:

將系統語言設置成英文,對提高英語水平有幫助嗎?
OpenStack 和 Hadoop 的區別是什麼?
為什麼阿里巴巴的持久層採用iBatis框架,而不使用hibernate框架呢?感覺hibernate更厲害的樣子?
本科國貿想讀計算機軟體與理論專業研究生?
未來你會選擇微軟的 Surface 平板電腦嗎?

TAG:編程 | 計算機 | Java | Java虛擬機JVM | 編譯 |