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函数。


更多文章
  • Redis源码阅读:bitmap 位图的运算
  • Redis源码阅读:set是怎么做交并集运算的?
  • Redis源码阅读:list实现(ziplist, quicklist)
  • Redis源码阅读:RDB是怎么实现的
  • Redis源码阅读:AOF重写
  • Redis源码阅读:AOF持久化
  • Redis源码阅读:key是怎么过期的
  • Redis源码阅读:字典是怎么实现的
  • Redis源码阅读:执行命令
  • Redis源码阅读:启动过程
  • WAL(Write-ahead logging)的套路
  • 搞定CORS问题
  • 如何定位程序问题所在
  • 设计一个IM归档系统
  • logrotate read only filesystem问题