slice是什麼時候決定要擴張?
slice是什麼時候決定要擴張?
網上說slice的文章已經很多了,大都已經把slice的記憶體擴張原理都說清楚了。但是是如何判斷slice是否需要擴張這個點卻沒有說的很清楚。想當然的我會覺得這個append是否擴張的邏輯應該隱藏在runtime中的某個函式,根據append的陣列的長度進行判斷。但是是否是如此呢?
本著這個疑問,我做了如下的實驗。
我寫了兩個方法,一個需要擴張,一個不需要擴張。
無需擴張
不需要擴張的程式碼如下:
package main func main() { a := make([]int, 1, 3) a = append(a, 4) println(a) }
使用 go tool objdump 來打印出編譯後的main彙編碼:
TEXT main.main(SB) /Users/yejianfeng/Documents/gopath/src/demo/append.go append.go:30x104e14065488b0c2530000000MOVQ GS:0x30, CX append.go:30x104e149483b6110CMPQ 0x10(CX), SP append.go:30x104e14d7661JBE 0x104e1b0 append.go:30x104e14f4883ec38SUBQ $0x38, SP append.go:30x104e15348896c2430MOVQ BP, 0x30(SP) append.go:30x104e158488d6c2430LEAQ 0x30(SP), BP append.go:40x104e15d48c744241800000000MOVQ $0x0, 0x18(SP) append.go:40x104e1660f57c0XORPS X0, X0 append.go:40x104e1690f11442420MOVUPS X0, 0x20(SP) append.go:50x104e16e48c744242004000000MOVQ $0x4, 0x20(SP) append.go:60x104e177e86445fdffCALL runtime.printlock(SB) append.go:60x104e17c488d442418LEAQ 0x18(SP), AX append.go:60x104e18148890424MOVQ AX, 0(SP) append.go:60x104e18548c744240802000000MOVQ $0x2, 0x8(SP) append.go:60x104e18e48c744241003000000MOVQ $0x3, 0x10(SP) append.go:60x104e197e8f44efdffCALL runtime.printslice(SB) append.go:60x104e19ce8bf47fdffCALL runtime.printnl(SB) append.go:60x104e1a1e8ba45fdffCALL runtime.printunlock(SB) append.go:70x104e1a6488b6c2430MOVQ 0x30(SP), BP append.go:70x104e1ab4883c438ADDQ $0x38, SP append.go:70x104e1afc3RET append.go:30x104e1b0e82b89ffffCALL runtime.morestack_noctxt(SB) append.go:30x104e1b5eb89JMP main.main(SB)
這個彙編碼的邏輯在append.go第5行就只有一個MOV指令,將4直接放到指定的記憶體地址。
需要擴張
我的另一個需要擴張的程式碼如下:
package main func main() { a := make([]int, 1, 1) a = append(a, 4) println(a) }
生成的彙編碼如下:
TEXT main.main(SB) /Users/yejianfeng/Documents/gopath/src/demo/append.go append.go:30x104e14065488b0c2530000000MOVQ GS:0x30, CX append.go:30x104e149483b6110CMPQ 0x10(CX), SP append.go:30x104e14d0f86b0000000JBE 0x104e203 append.go:30x104e1534883ec68SUBQ $0x68, SP append.go:30x104e15748896c2460MOVQ BP, 0x60(SP) append.go:30x104e15c488d6c2460LEAQ 0x60(SP), BP append.go:50x104e16148c744245000000000MOVQ $0x0, 0x50(SP) append.go:50x104e16a488d05af9d0000LEAQ type.*+40128(SB), AX append.go:50x104e17148890424MOVQ AX, 0(SP) append.go:50x104e175488d442450LEAQ 0x50(SP), AX append.go:50x104e17a4889442408MOVQ AX, 0x8(SP) append.go:50x104e17f48c744241001000000MOVQ $0x1, 0x10(SP) append.go:50x104e18848c744241801000000MOVQ $0x1, 0x18(SP) append.go:50x104e19148c744242002000000MOVQ $0x2, 0x20(SP) append.go:50x104e19ae8b16bfeffCALL runtime.growslice(SB) append.go:50x104e19f488b442428MOVQ 0x28(SP), AX append.go:50x104e1a44889442458MOVQ AX, 0x58(SP) append.go:50x104e1a9488b4c2430MOVQ 0x30(SP), CX append.go:50x104e1ae48894c2448MOVQ CX, 0x48(SP) append.go:50x104e1b3488b542438MOVQ 0x38(SP), DX append.go:50x104e1b84889542440MOVQ DX, 0x40(SP) append.go:50x104e1bd48c7400804000000MOVQ $0x4, 0x8(AX) append.go:60x104e1c5e81645fdffCALL runtime.printlock(SB) append.go:60x104e1ca488b442458MOVQ 0x58(SP), AX append.go:60x104e1cf48890424MOVQ AX, 0(SP) append.go:50x104e1d3488b442448MOVQ 0x48(SP), AX append.go:50x104e1d848ffc0INCQ AX append.go:60x104e1db4889442408MOVQ AX, 0x8(SP) append.go:60x104e1e0488b442440MOVQ 0x40(SP), AX append.go:60x104e1e54889442410MOVQ AX, 0x10(SP) append.go:60x104e1eae8a14efdffCALL runtime.printslice(SB) append.go:60x104e1efe86c47fdffCALL runtime.printnl(SB) append.go:60x104e1f4e86745fdffCALL runtime.printunlock(SB) append.go:70x104e1f9488b6c2460MOVQ 0x60(SP), BP append.go:70x104e1fe4883c468ADDQ $0x68, SP
這裡的第5行就和之前的那個大不一樣了。有非常多的邏輯。基本進入第五行做的事情就是開始準備呼叫runtime.growslice的邏輯了
append.go:50x104e16148c744245000000000MOVQ $0x0, 0x50(SP) append.go:50x104e16a488d05af9d0000LEAQ type.*+40128(SB), AX append.go:50x104e17148890424MOVQ AX, 0(SP) append.go:50x104e175488d442450LEAQ 0x50(SP), AX append.go:50x104e17a4889442408MOVQ AX, 0x8(SP) append.go:50x104e17f48c744241001000000MOVQ $0x1, 0x10(SP) append.go:50x104e18848c744241801000000MOVQ $0x1, 0x18(SP) append.go:50x104e19148c744242002000000MOVQ $0x2, 0x20(SP) append.go:50x104e19ae8b16bfeffCALL runtime.growslice(SB)
這裡就很明顯了,所以slice的append是否進行cap擴張是在編譯器進行判斷的?至少我上面的兩個程式碼,編譯器編譯的時候是知道這個slice是否需要進行擴張的,根據是否進行擴張就決定是否呼叫growslice。
再複雜的case
在雨痕群裡問了下這個問題,有位群友給了個更為複雜點的case:
package main func main() { a := make([]int, 1, 5) b := 3 for i := 0; i < b; i++ { a = append(a, 4) } println(a) }
這裡的append是包圍在for迴圈裡面的,編譯器其實就很難判斷了。我們看下彙編:
TEXT main.main(SB) /Users/yejianfeng/Documents/gopath/src/demo/append.go append.go:30x104e14065488b0c2530000000MOVQ GS:0x30, CX append.go:30x104e149488d4424f0LEAQ -0x10(SP), AX append.go:30x104e14e483b4110CMPQ 0x10(CX), AX append.go:30x104e1520f86fb000000JBE 0x104e253 append.go:30x104e1584881ec90000000SUBQ $0x90, SP append.go:30x104e15f4889ac2488000000MOVQ BP, 0x88(SP) append.go:30x104e167488dac2488000000LEAQ 0x88(SP), BP append.go:40x104e16f48c744245800000000MOVQ $0x0, 0x58(SP) append.go:40x104e1780f57c0XORPS X0, X0 append.go:40x104e17b0f11442460MOVUPS X0, 0x60(SP) append.go:40x104e1800f11442470MOVUPS X0, 0x70(SP) append.go:40x104e18531c0XORL AX, AX append.go:40x104e187488d4c2458LEAQ 0x58(SP), CX append.go:40x104e18cba01000000MOVL $0x1, DX append.go:40x104e191bb05000000MOVL $0x5, BX append.go:60x104e196eb0eJMP 0x104e1a6 append.go:70x104e19848c704d104000000MOVQ $0x4, 0(CX)(DX*8) append.go:60x104e1a048ffc0INCQ AX append.go:90x104e1a34889f2MOVQ SI, DX append.go:90x104e1a64889542448MOVQ DX, 0x48(SP) append.go:60x104e1ab4883f803CMPQ $0x3, AX append.go:60x104e1af7d51JGE 0x104e202 append.go:70x104e1b1488d7201LEAQ 0x1(DX), SI append.go:70x104e1b54839deCMPQ BX, SI append.go:70x104e1b87edeJLE 0x104e198 append.go:60x104e1ba4889442440MOVQ AX, 0x40(SP) append.go:70x104e1bf488d05ba9d0000LEAQ type.*+40128(SB), AX append.go:70x104e1c648890424MOVQ AX, 0(SP) append.go:70x104e1ca48894c2408MOVQ CX, 0x8(SP) append.go:70x104e1cf4889542410MOVQ DX, 0x10(SP) append.go:70x104e1d448895c2418MOVQ BX, 0x18(SP) append.go:70x104e1d94889742420MOVQ SI, 0x20(SP) append.go:70x104e1dee86d6bfeffCALL runtime.growslice(SB) append.go:70x104e1e3488b4c2428MOVQ 0x28(SP), CX append.go:70x104e1e8488b442430MOVQ 0x30(SP), AX append.go:70x104e1ed488b5c2438MOVQ 0x38(SP), BX append.go:70x104e1f2488d7001LEAQ 0x1(AX), SI append.go:60x104e1f6488b442440MOVQ 0x40(SP), AX append.go:70x104e1fb488b542448MOVQ 0x48(SP), DX append.go:70x104e200eb96JMP 0x104e198 append.go:90x104e20248898c2480000000MOVQ CX, 0x80(SP) append.go:90x104e20a48895c2450MOVQ BX, 0x50(SP) append.go:90x104e20fe8cc44fdffCALL runtime.printlock(SB)
重點看這一行:
append.go:70x104e1b54839deCMPQ BX, SI append.go:70x104e1b87edeJLE 0x104e198
BX裡面存的是a現在的cap值,(可以從MOVL $0x5, BX
看出來)。而SI裡面儲存的是老的slice的長度(DX)加1之後的值,就是新的slice需要的len值。所以上面兩句的意思就是比較下新的len和cap的大小,如果len小於cap的話,就跳到0x104e198,就是直接執行MOVE操作,否則的話,就開始準備growslice。
總結
上面的分析說明,slice是否需要擴張的邏輯是編譯器做的,並且編譯器如果能直接判斷是否這個slice需要擴張,就直接將是否需要擴張的結果作為編譯結果。否則的話,就將這個if else的邏輯寫在編譯結果裡面,在runtime時候跳轉判斷。
到這裡我有點理解編譯器和執行時的邊界。其實本質上,兩個步驟都是為了程式碼更快得出結果,編譯器優化的越多,執行過程執行的速度就越快,當然編譯器同時也需要兼顧生成的可執行檔案的大小問題等。對一個語言,編譯器優化,是個很重要的工作。