Go的slice工作機制
slice是咋工作的?首先我們從一個demo看起:
package main import ( "fmt" ) func main() { a := make([]int, 10) fmt.Printf("%p\n", &a[0]) for i := 0; i < 100; i++ { a = append(a, 1) fmt.Printf("%p\n", &a[0]) } }
用gdb在 a = append(a, 1)
這一行下個斷點,執行:
(gdb) list 1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func main() { 8 a := make([]int, 10) 9 fmt.Printf("%p\n", &a[0]) 10 for i := 0; i < 100; i++ { (gdb) 11 a = append(a, 1) 12 fmt.Printf("%p\n", &a[0]) 13 } 14 } (gdb) b 11 Breakpoint 1 at 0x4af7d8: file /home/jiajun/Code/test/main.go, line 11. (gdb) run Starting program: /home/jiajun/Code/test/test [New LWP 12218] [New LWP 12219] [New LWP 12220] [New LWP 12221] 0xc000130000 Thread 1 "test" hit Breakpoint 1, main.main () at /home/jiajun/Code/test/main.go:11 11 a = append(a, 1) (gdb) s runtime.growslice (et=0x4bd560, old=..., cap=11, ~r3=...) at /snap/go/5759/src/runtime/slice.go:76 76 func growslice(et *_type, old slice, cap int) slice { (gdb) quit
可以看到調用了 slice.go 裏的 growslice
函數:
func growslice(et *_type, old slice, cap int) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += newcap / 4 } if newcap <= 0 { newcap = cap } } } var overflow bool var lenmem, newlenmem, capmem uintptr switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) } if overflow || capmem > maxAlloc { panic(errorString("growslice: cap out of range")) } var p unsafe.Pointer if et.ptrdata == 0 { p = mallocgc(capmem, nil, false) memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory. p = mallocgc(capmem, et, true) if lenmem > 0 && writeBarrier.enabled { bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem) } } memmove(p, old.array, lenmem) return slice{p, old.len, newcap} }
上述代碼,是執行append時的代碼,但是,從最後幾行來看,豈不是每次都新申請了一塊內存?我們來執行一下最開始的demo看看:
$ go run main.go 0xc000130000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 0xc000134000 ...
可以看到,這裏打出來的內存地址,並不是每次都不一樣的,而且如果真的這樣做,那麼append的性能就非常低,所以,growslice 函數只是在容量不足時,纔會調用,而平時追加值,可能是直接在彙編裏完成的,我們來看看彙編碼:
$ go tool compile -N -S main.go | grep main.go:11 0x025e 00606 (main.go:11) MOVQ "".a+376(SP), DX 0x0266 00614 (main.go:11) LEAQ 1(DX), BX 0x026a 00618 (main.go:11) PCDATA $0, $5 0x026a 00618 (main.go:11) MOVQ "".a+368(SP), SI 0x0272 00626 (main.go:11) PCDATA $1, $0 0x0272 00626 (main.go:11) MOVQ "".a+384(SP), DI 0x027a 00634 (main.go:11) CMPQ BX, DI 0x027d 00637 (main.go:11) JLS 644 0x027f 00639 (main.go:11) JMP 1167 0x0284 00644 (main.go:11) PCDATA $0, $-1 0x0284 00644 (main.go:11) PCDATA $1, $-1 0x0284 00644 (main.go:11) JMP 646 0x0286 00646 (main.go:11) PCDATA $0, $5 0x0286 00646 (main.go:11) PCDATA $1, $0 0x0286 00646 (main.go:11) MOVQ $1, (SI)(DX*8) 0x028e 00654 (main.go:11) PCDATA $1, $1 0x028e 00654 (main.go:11) MOVQ SI, "".a+368(SP) 0x0296 00662 (main.go:11) MOVQ BX, "".a+376(SP) 0x029e 00670 (main.go:11) MOVQ DI, "".a+384(SP) 0x048f 01167 (main.go:11) PCDATA $0, $5 0x048f 01167 (main.go:11) PCDATA $1, $0 0x048f 01167 (main.go:11) MOVQ DX, ""..autotmp_27+120(SP) 0x0494 01172 (main.go:11) PCDATA $0, $6 0x0494 01172 (main.go:11) LEAQ type.int(SB), AX 0x049b 01179 (main.go:11) PCDATA $0, $5 0x049b 01179 (main.go:11) MOVQ AX, (SP) 0x049f 01183 (main.go:11) PCDATA $0, $0 0x049f 01183 (main.go:11) MOVQ SI, 8(SP) 0x04a4 01188 (main.go:11) MOVQ DX, 16(SP) 0x04a9 01193 (main.go:11) MOVQ DI, 24(SP) 0x04ae 01198 (main.go:11) MOVQ BX, 32(SP) 0x04b3 01203 (main.go:11) CALL runtime.growslice(SB) 0x04b8 01208 (main.go:11) PCDATA $0, $5 0x04b8 01208 (main.go:11) MOVQ 40(SP), SI 0x04bd 01213 (main.go:11) MOVQ 48(SP), AX 0x04c2 01218 (main.go:11) MOVQ 56(SP), DI 0x04c7 01223 (main.go:11) LEAQ 1(AX), BX 0x04cb 01227 (main.go:11) MOVQ ""..autotmp_27+120(SP), DX 0x04d0 01232 (main.go:11) JMP 646
emmm,可以仔細品味一下這段彙編碼,可以看到幾個JMP指令,這是跳轉指令。還有CMPQ指令,這是判斷指令,雖然不能完全看懂生成 出來的彙編碼,但是結合上面的試驗結果,基本印證了我們的猜測,即append操作是通過彙編完成的,只有當容量不足時,纔會調用 growslice函數。