思考
相信大家在實際的項目開發中會遇到這么一個事,有的程序員寫的代碼不僅bug少,而且性能高;而有的程序員寫的代碼能否流暢的跑起來,都是一個很大問題。
而我們今天要討論的就是一個關於性能優化的案例分析。
案例分析
我們先來構造一些基礎數據(長度為10億的切片,並賦上值):
var testData = GenerateData() // generate billion slice data func GenerateData() []int { data := make([]int, 1000000000) for key, _ := range data { data[key] = key % 128 } return data } // get length func GetDataLen() int { return len(testData) }
案例一
// case one func CaseSumOne(result *int) { data := GenerateData() for i := 0; i < GetDataLen(); i++ { *result += data[i] } } // case two func CaseSumTwo(result *int) { data := GenerateData() dataLen := GetDataLen() for i := 0; i < dataLen; i++ { *result += data[i] } }
執行結果
$ go test -bench=. goos: windows goarch: amd64 BenchmarkCaseSumOne-8 1 7439749000 ns/op BenchmarkCaseSumTwo-8 1 2529266700 ns/op PASS ok _/C_/go-code/perform/case-one 14.059s
問題分析
- CaseSumTwo執行效率是CaseSumOne的2.94倍,快了近三倍,這是為什么呢?
- 我想這個其實很容易猜到,這里有一個連續的函數調用“GetDataLen()”,
我們來看下兩個函數的匯編,做個簡單的對比:
函數CaseSumOne
"".CaseSumOne STEXT size=83 args=0x4 locals=0xc 0x0000 00000 (point.go:22) TEXT "".CaseSumOne(SB), $12-4 ... // point.go:24 -> for i := 0; i < GetDataLen(); i++ 0x0021 00033 (point.go:24) PCDATA $2, $2 0x0021 00033 (point.go:24) PCDATA $0, $1 0x0021 00033 (point.go:24) MOVL "".result+16(SP), DX 0x0025 00037 (point.go:24) XORL BX, BX 0x0027 00039 (point.go:24) JMP 47 0x0029 00041 (point.go:25) MOVL (CX)(BX*4), BP // CX循環計數器 0x002c 00044 (point.go:25) ADDL BP, (DX) 0x002e 00046 (point.go:24) INCL BX // i++ 0x002f 00047 (point.go:24) MOVL "".testData+4(SB), BP // 棧指針寄存器 0x0035 00053 (point.go:24) CMPL BX, BP 0x0037 00055 (point.go:24) JGE 65 ... 0x0045 00069 (point.go:25) CALL runtime.panicindex(SB) 0x004c 00076 (point.go:22) CALL runtime.morestack_noctxt(SB) ...
函數CaseSumTwo
"".CaseSumTwo STEXT size=83 args=0x4 locals=0xc 0x0000 00000 (point.go:30) TEXT "".CaseSumTwo(SB), $12-4 ... // point.go:32 -> dataLen := GetDataLen() // point.go:33 -> for i := 0; i < dataLen; i++ { 0x0021 00033 (point.go:32) MOVL "".testData+4(SB), DX 0x0027 00039 (point.go:33) PCDATA $2, $2 0x0027 00039 (point.go:33) PCDATA $0, $1 0x0027 00039 (point.go:33) MOVL "".result+16(SP), BX 0x002b 00043 (point.go:33) XORL BP, BP 0x002d 00045 (point.go:33) JMP 53 0x002f 00047 (point.go:34) MOVL (AX)(BP*4), SI 0x0032 00050 (point.go:34) ADDL SI, (BX) 0x0034 00052 (point.go:33) INCL BP 0x0035 00053 (point.go:33) CMPL BP, DX 0x0037 00055 (point.go:33) JGE 65 ... 0x0045 00069 (point.go:34) CALL runtime.panicindex(SB) 0x004c 00076 (point.go:30) CALL runtime.morestack_noctxt(SB) ...
比較結論
不難發現主要的區別是在CaseSumOne中多了這么一行:
0x002f 00047 (point.go:24) MOVL "".testData+4(SB), BP
其實雖然只有一行,但是對於函數“GetDataLen”里需要調用的指令對CPU的消耗:
"".GetDataLen STEXT size=36 args=0x4 locals=0x0 0x0000 00000 (point.go:17) TEXT "".GetDataLen(SB), $0-4 // 0x0000 00000 (point.go:17) MOVL TLS, CX 0x0007 00007 (point.go:17) MOVL (CX)(TLS*2), CX 0x000d 00013 (point.go:17) CMPL SP, 8(CX) 0x0010 00016 (point.go:17) JLS 29 0x0012 00018 (point.go:17) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0012 00018 (point.go:17) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0012 00018 (point.go:17) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0012 00018 (point.go:18) PCDATA $2, $0 0x0012 00018 (point.go:18) PCDATA $0, $0 0x0012 00018 (point.go:18) MOVL "".testData+4(SB), AX // 寄存器尋址 AX = lenVAL 0x0018 00024 (point.go:18) MOVL AX, "".~r0+4(SP) // SP = AX = lenVal 0x001c 00028 (point.go:18) RET 0x001d 00029 (point.go:18) NOP 0x001d 00029 (point.go:17) PCDATA $0, $-1 0x001d 00029 (point.go:17) PCDATA $2, $-1 0x001d 00029 (point.go:17) CALL runtime.morestack_noctxt(SB) // 壓棧 ...
雖然,看似小小一行代碼的區別,但是在指令級的角度上,進行了創建棧空間、壓棧、尋址、賦值等一系列操作,況且這里進行了循環調用。
案例二
// case two func CaseSumTwo(result *int) { data := GenerateData() dataLen := GetDataLen() for i := 0; i < dataLen; i++ { *result += data[i] } } // case three func CaseSumThree(result *int) { data := GenerateData() dataLen := GetDataLen() tmp := *result for i:= 0; i < dataLen; i++ { tmp += data[i] } *result = tmp }
執行結果
$ go test -bench=. goos: windows goarch: amd64 BenchmarkCaseSumTwo-8 1 2529266700 ns/op BenchmarkCaseSumThree-8 1 1657554600 ns/op PASS ok _/C_/go-code/perform/case-one 8.2773
問題分析
- 雖然對連續函數調用進行了優化,但是CaseSumThree對執行效率還是高於CaseSumTwo1.52倍,還有哪些情況會影響執行性能呢?
我們再來對比下“CaseSumTwo”和“CaseSumThree”對匯編源碼:
函數CaseSumTwo
"".CaseSumTwo STEXT size=83 args=0x4 locals=0xc 0x0000 00000 (point.go:30) TEXT "".CaseSumTwo(SB), $12-4 ... // point.go:31 -> data := GenerateData() // point.go:34 -> *result += data[i] 0x001a 00026 (point.go:31) MOVL (SP), AX 0x0027 00039 (point.go:33) MOVL "".result+16(SP), BX 0x002f 00047 (point.go:34) MOVL (AX)(BP*4), SI // 棧寄存器移動四個字節, -> SI源變址寄存器 0x0032 00050 (point.go:34) ADDL SI, (BX) // SI 0x0034 00052 (point.go:33) INCL BP 0x0035 00053 (point.go:33) CMPL BP, DX 0x0037 00055 (point.go:33) JGE 65 0x0039 00057 (point.go:34) TESTB AX, (BX) 0x003b 00059 (point.go:34) CMPL BP, CX 0x003d 00061 (point.go:34) JCS 47 0x003f 00063 (point.go:34) JMP 69 0x0041 00065 (<unknown line number>) PCDATA $2, $-2 0x0041 00065 (<unknown line number>) PCDATA $0, $-2 0x0041 00065 (<unknown line number>) ADDL $12, SP 0x0044 00068 (<unknown line number>) RET 0x0045 00069 (point.go:34) PCDATA $2, $0 0x0045 00069 (point.go:34) PCDATA $0, $1 0x0045 00069 (point.go:34) CALL runtime.panicindex(SB) 0x004a 00074 (point.go:34) UNDEF 0x004c 00076 (point.go:34) NOP
函數CaseSumThree
"".CaseSumThree STEXT size=97 args=0x4 locals=0x10 0x0000 00000 (point.go:39) TEXT "".CaseSumThree(SB), $16-4 ... // point.go:40 -> data := GenerateData() // point.go:42 -> tmp := *result // point.go:44 -> tmp += data[i] // point.go:46 -> *result = tmp 0x001a 00026 (point.go:40) MOVL (SP), AX 0x0021 00033 (point.go:42) PCDATA $2, $2 0x0021 00033 (point.go:42) PCDATA $0, $1 0x0021 00033 (point.go:42) MOVL "".result+20(SP), DX 0x0025 00037 (point.go:42) MOVL (DX), BX // ->BX數據指針寄存器 0x0027 00039 (point.go:41) MOVL "".testData+4(SB), BP 0x002d 00045 (point.go:41) XORL SI, SI 0x002f 00047 (point.go:43) JMP 67 0x0031 00049 (point.go:43) LEAL 1(SI), DI 0x0034 00052 (point.go:43) MOVL DI, "".i+12(SP) // 移動DI到棧指針12字節的位置 0x0038 00056 (point.go:44) MOVL (AX)(SI*4), DI // 源變址寄存器移動四個字節(32位),-> 目的變址寄存器 0x003b 00059 (point.go:44) ADDL DI, BX // DI+BX 0x003d 00061 (point.go:43) MOVL "".i+12(SP), DI 0x0041 00065 (point.go:43) MOVL DI, SI 0x0043 00067 (point.go:43) CMPL SI, BP 0x0045 00069 (point.go:43) JGE 77 0x0047 00071 (point.go:44) CMPL SI, CX 0x0049 00073 (point.go:44) JCS 49 0x004b 00075 (point.go:44) JMP 83 0x004d 00077 (point.go:46) PCDATA $2, $0 0x004d 00077 (point.go:46) MOVL BX, (DX) 0x004f 00079 (point.go:47) ADDL $16, SP 0x0052 00082 (point.go:47) RET 0x0053 00083 (point.go:44) CALL runtime.panicindex(SB) ...
比較結論
CaseSumTwo函數,在進行ADDL之前,因為“*result”為指針變量,所以不能直接與data[i]運算。因此需要創建一個棧空間,並指向data的地址並,然后通過移動棧指針后得到下一個值的地址,並賦與SI。
CaseSumThree函數,在進行ADDL執行前,創建了一個值變量,那么在執行ADDL的時候,只需要移動SI獲取下一個data的值就可以直接進行算數運算,中間少了地址的引用的棧的操作。
堆和棧
其實說白了,就是CaseSumTwo中 *result
內存是分配在堆上的,而 CaseSumThree中 tmp
是分配在棧上的,而堆和棧堆性能區別這里做一個簡單堆比較:
-
有寄存器直接對棧進行訪問(esp,ebp),而對堆訪問,只能是間接尋址。 也就是說,可以直接從地址取數據放至目標地址;使用堆時,第一步將分配的地址放到寄存器,然后取出這個地址的值,然后放到目標地址。
-
棧中數據cpu命中率更高,滿足局部性原理。
-
棧是編譯時系統自動分配空間,而堆是動態分配(運行時分配空間),所以棧的速度快。
-
總結
本章主要講了三個點:
- 消除循環的低效率
- 減少過程調用
- 消除不必要的內存引用
引用《深入計算機系統原理》一書中對性能優化所提到的三個方面:
- 高級設計,為遇到的問題選擇適當的算法和數據結構。要特別警覺,避免使用那些會漸進地產生糟糕性能的算法或編碼技術。
- 基本編碼原則,從指令的角度考慮,開發中應如何編碼,才能減少執行的指令。
- 低級優化,針對現代處理器,如何讓cpu的流水線盡量飽合。
所以,一個優秀的程序員在寫每一行代碼,定義每一個變量,也許背后思考的就會更多。