代码改变世界

slice是什么时候决定要扩张?

2019-04-16 08:58  轩脉刃  阅读(...)  评论(...编辑  收藏

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:3		0x104e140		65488b0c2530000000	MOVQ GS:0x30, CX
  append.go:3		0x104e149		483b6110		CMPQ 0x10(CX), SP
  append.go:3		0x104e14d		7661			JBE 0x104e1b0
  append.go:3		0x104e14f		4883ec38		SUBQ $0x38, SP
  append.go:3		0x104e153		48896c2430		MOVQ BP, 0x30(SP)
  append.go:3		0x104e158		488d6c2430		LEAQ 0x30(SP), BP
  append.go:4		0x104e15d		48c744241800000000	MOVQ $0x0, 0x18(SP)
  append.go:4		0x104e166		0f57c0			XORPS X0, X0
  append.go:4		0x104e169		0f11442420		MOVUPS X0, 0x20(SP)
  append.go:5		0x104e16e		48c744242004000000	MOVQ $0x4, 0x20(SP)
  append.go:6		0x104e177		e86445fdff		CALL runtime.printlock(SB)
  append.go:6		0x104e17c		488d442418		LEAQ 0x18(SP), AX
  append.go:6		0x104e181		48890424		MOVQ AX, 0(SP)
  append.go:6		0x104e185		48c744240802000000	MOVQ $0x2, 0x8(SP)
  append.go:6		0x104e18e		48c744241003000000	MOVQ $0x3, 0x10(SP)
  append.go:6		0x104e197		e8f44efdff		CALL runtime.printslice(SB)
  append.go:6		0x104e19c		e8bf47fdff		CALL runtime.printnl(SB)
  append.go:6		0x104e1a1		e8ba45fdff		CALL runtime.printunlock(SB)
  append.go:7		0x104e1a6		488b6c2430		MOVQ 0x30(SP), BP
  append.go:7		0x104e1ab		4883c438		ADDQ $0x38, SP
  append.go:7		0x104e1af		c3			RET
  append.go:3		0x104e1b0		e82b89ffff		CALL runtime.morestack_noctxt(SB)
  append.go:3		0x104e1b5		eb89			JMP 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:3		0x104e140		65488b0c2530000000	MOVQ GS:0x30, CX
  append.go:3		0x104e149		483b6110		CMPQ 0x10(CX), SP
  append.go:3		0x104e14d		0f86b0000000		JBE 0x104e203
  append.go:3		0x104e153		4883ec68		SUBQ $0x68, SP
  append.go:3		0x104e157		48896c2460		MOVQ BP, 0x60(SP)
  append.go:3		0x104e15c		488d6c2460		LEAQ 0x60(SP), BP
  append.go:5		0x104e161		48c744245000000000	MOVQ $0x0, 0x50(SP)
  append.go:5		0x104e16a		488d05af9d0000		LEAQ type.*+40128(SB), AX
  append.go:5		0x104e171		48890424		MOVQ AX, 0(SP)
  append.go:5		0x104e175		488d442450		LEAQ 0x50(SP), AX
  append.go:5		0x104e17a		4889442408		MOVQ AX, 0x8(SP)
  append.go:5		0x104e17f		48c744241001000000	MOVQ $0x1, 0x10(SP)
  append.go:5		0x104e188		48c744241801000000	MOVQ $0x1, 0x18(SP)
  append.go:5		0x104e191		48c744242002000000	MOVQ $0x2, 0x20(SP)
  append.go:5		0x104e19a		e8b16bfeff		CALL runtime.growslice(SB)
  append.go:5		0x104e19f		488b442428		MOVQ 0x28(SP), AX
  append.go:5		0x104e1a4		4889442458		MOVQ AX, 0x58(SP)
  append.go:5		0x104e1a9		488b4c2430		MOVQ 0x30(SP), CX
  append.go:5		0x104e1ae		48894c2448		MOVQ CX, 0x48(SP)
  append.go:5		0x104e1b3		488b542438		MOVQ 0x38(SP), DX
  append.go:5		0x104e1b8		4889542440		MOVQ DX, 0x40(SP)
  append.go:5		0x104e1bd		48c7400804000000	MOVQ $0x4, 0x8(AX)
  append.go:6		0x104e1c5		e81645fdff		CALL runtime.printlock(SB)
  append.go:6		0x104e1ca		488b442458		MOVQ 0x58(SP), AX
  append.go:6		0x104e1cf		48890424		MOVQ AX, 0(SP)
  append.go:5		0x104e1d3		488b442448		MOVQ 0x48(SP), AX
  append.go:5		0x104e1d8		48ffc0			INCQ AX
  append.go:6		0x104e1db		4889442408		MOVQ AX, 0x8(SP)
  append.go:6		0x104e1e0		488b442440		MOVQ 0x40(SP), AX
  append.go:6		0x104e1e5		4889442410		MOVQ AX, 0x10(SP)
  append.go:6		0x104e1ea		e8a14efdff		CALL runtime.printslice(SB)
  append.go:6		0x104e1ef		e86c47fdff		CALL runtime.printnl(SB)
  append.go:6		0x104e1f4		e86745fdff		CALL runtime.printunlock(SB)
  append.go:7		0x104e1f9		488b6c2460		MOVQ 0x60(SP), BP
  append.go:7		0x104e1fe		4883c468		ADDQ $0x68, SP

这里的第5行就和之前的那个大不一样了。有非常多的逻辑。基本进入第五行做的事情就是开始准备调用runtime.growslice的逻辑了

append.go:5		0x104e161		48c744245000000000	MOVQ $0x0, 0x50(SP)
append.go:5		0x104e16a		488d05af9d0000		LEAQ type.*+40128(SB), AX
append.go:5		0x104e171		48890424		MOVQ AX, 0(SP)
append.go:5		0x104e175		488d442450		LEAQ 0x50(SP), AX
append.go:5		0x104e17a		4889442408		MOVQ AX, 0x8(SP)
append.go:5		0x104e17f		48c744241001000000	MOVQ $0x1, 0x10(SP)
append.go:5		0x104e188		48c744241801000000	MOVQ $0x1, 0x18(SP)
append.go:5		0x104e191		48c744242002000000	MOVQ $0x2, 0x20(SP)
append.go:5		0x104e19a		e8b16bfeff		CALL 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:3		0x104e140		65488b0c2530000000	MOVQ GS:0x30, CX
  append.go:3		0x104e149		488d4424f0		LEAQ -0x10(SP), AX
  append.go:3		0x104e14e		483b4110		CMPQ 0x10(CX), AX
  append.go:3		0x104e152		0f86fb000000		JBE 0x104e253
  append.go:3		0x104e158		4881ec90000000		SUBQ $0x90, SP
  append.go:3		0x104e15f		4889ac2488000000	MOVQ BP, 0x88(SP)
  append.go:3		0x104e167		488dac2488000000	LEAQ 0x88(SP), BP
  append.go:4		0x104e16f		48c744245800000000	MOVQ $0x0, 0x58(SP)
  append.go:4		0x104e178		0f57c0			XORPS X0, X0
  append.go:4		0x104e17b		0f11442460		MOVUPS X0, 0x60(SP)
  append.go:4		0x104e180		0f11442470		MOVUPS X0, 0x70(SP)
  append.go:4		0x104e185		31c0			XORL AX, AX
  append.go:4		0x104e187		488d4c2458		LEAQ 0x58(SP), CX
  append.go:4		0x104e18c		ba01000000		MOVL $0x1, DX
  append.go:4		0x104e191		bb05000000		MOVL $0x5, BX
  append.go:6		0x104e196		eb0e			JMP 0x104e1a6
  append.go:7		0x104e198		48c704d104000000	MOVQ $0x4, 0(CX)(DX*8)
  append.go:6		0x104e1a0		48ffc0			INCQ AX
  append.go:9		0x104e1a3		4889f2			MOVQ SI, DX
  append.go:9		0x104e1a6		4889542448		MOVQ DX, 0x48(SP)
  append.go:6		0x104e1ab		4883f803		CMPQ $0x3, AX
  append.go:6		0x104e1af		7d51			JGE 0x104e202
  append.go:7		0x104e1b1		488d7201		LEAQ 0x1(DX), SI
  append.go:7		0x104e1b5		4839de			CMPQ BX, SI
  append.go:7		0x104e1b8		7ede			JLE 0x104e198
  append.go:6		0x104e1ba		4889442440		MOVQ AX, 0x40(SP)
  append.go:7		0x104e1bf		488d05ba9d0000		LEAQ type.*+40128(SB), AX
  append.go:7		0x104e1c6		48890424		MOVQ AX, 0(SP)
  append.go:7		0x104e1ca		48894c2408		MOVQ CX, 0x8(SP)
  append.go:7		0x104e1cf		4889542410		MOVQ DX, 0x10(SP)
  append.go:7		0x104e1d4		48895c2418		MOVQ BX, 0x18(SP)
  append.go:7		0x104e1d9		4889742420		MOVQ SI, 0x20(SP)
  append.go:7		0x104e1de		e86d6bfeff		CALL runtime.growslice(SB)
  append.go:7		0x104e1e3		488b4c2428		MOVQ 0x28(SP), CX
  append.go:7		0x104e1e8		488b442430		MOVQ 0x30(SP), AX
  append.go:7		0x104e1ed		488b5c2438		MOVQ 0x38(SP), BX
  append.go:7		0x104e1f2		488d7001		LEAQ 0x1(AX), SI
  append.go:6		0x104e1f6		488b442440		MOVQ 0x40(SP), AX
  append.go:7		0x104e1fb		488b542448		MOVQ 0x48(SP), DX
  append.go:7		0x104e200		eb96			JMP 0x104e198
  append.go:9		0x104e202		48898c2480000000	MOVQ CX, 0x80(SP)
  append.go:9		0x104e20a		48895c2450		MOVQ BX, 0x50(SP)
  append.go:9		0x104e20f		e8cc44fdff		CALL runtime.printlock(SB)

重点看这一行:

  append.go:7		0x104e1b5		4839de			CMPQ BX, SI
  append.go:7		0x104e1b8		7ede			JLE 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时候跳转判断。

到这里我有点理解编译器和运行时的边界。其实本质上,两个步骤都是为了代码更快得出结果,编译器优化的越多,运行过程执行的速度就越快,当然编译器同时也需要兼顾生成的可执行文件的大小问题等。对一个语言,编译器优化,是个很重要的工作。