Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于 Go 逃逸分析 #222

Open
itcuihao opened this issue Mar 18, 2019 · 9 comments
Open

关于 Go 逃逸分析 #222

itcuihao opened this issue Mar 18, 2019 · 9 comments
Labels

Comments

@itcuihao
Copy link
Owner

itcuihao commented Mar 18, 2019

fmt.Println 为什么会导致Go 变量逃逸?


package main

import (
	"fmt"
)

func main() {
	a := 1
	fmt.Println(a)
}

go tool compile -S -m run.go 查看。

haoc7~/mygo/src/tests$ go tool compile -S -m t1.go 
t1.go:9:13: a escapes to heap
t1.go:9:13: main ... argument does not escape
"".main STEXT size=134 args=0x0 locals=0x48

变量 a 逃逸到堆。
如果不使用 fmt.Println 则不会如此,这是为什么呢?

@itcuihao itcuihao added the Go label Mar 18, 2019
@itcuihao
Copy link
Owner Author

itcuihao commented Mar 18, 2019

go build 工具中的 flag -gcflags '-m' 可以用来分析内存逃逸的情况汇总,最多可以提供 4 个 "-m", m 越多则表示分析的程度越详细,一般情况下我们可以采用两个 m 分析。

haoc7:~/mygo/src/tests$ go build -gcflags '-m -l' t1.go 
# command-line-arguments
./t1.go:7:13: a escapes to heap
./t1.go:7:13: main ... argument does not escape

haoc7:~/mygo/src/tests$ go build -gcflags '-m -m -l' t1.go 
# command-line-arguments
./t1.go:7:13: a escapes to heap
./t1.go:7:13:   from ... argument (arg to ...) at ./t1.go:7:13
./t1.go:7:13:   from *(... argument) (indirection) at ./t1.go:7:13
./t1.go:7:13:   from ... argument (passed to call[argument content escapes]) at ./t1.go:7:13
./t1.go:7:13: main ... argument does not escape

escapes to heap 则表明了变量逃逸到了堆(heap)上。其中 -l 表示不启用 inline 模式调用,否则会使得分析更加复杂,也可以在函数上方添加注释 //go:noinline禁止函数 inline调用。

关于 fmt.Println 导致变量逃逸的分析
1.fmt: Printf arguments escape to heap
2.runtime: don't allocate for non-escaping conversions to interface{}

@itcuihao
Copy link
Owner Author

Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。

如果函数外部没有引用,则优先放到栈中;

如果函数外部存在引用,则必定放到堆中;

@itcuihao
Copy link
Owner Author

堆上动态分配内存比栈上静态分配内存,开销大很多。

变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。

不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。

@itcuihao
Copy link
Owner Author

逃逸分析跟踪代码区域的变量范围,检查变量的生命周期,当检查不通过时,会分配至堆,称为逃逸。

@itcuihao
Copy link
Owner Author

The rules may continue to seem arbitrary at first, but after some trial and error with these tools, patterns do begin to emerge. For those short on time, here’s a list of some patterns we’ve found which typically cause variables to escape to the heap:

  • Sending pointers or values containing pointers to channels. At compile time there’s no way to know which goroutine will receive the data on a channel. Therefore the compiler cannot determine when this data will no longer be referenced.

  • Storing pointers or values containing pointers in a slice. An example of this is a type like []*string. This always causes the contents of the slice to escape. Even though the backing array of the slice may still be on the stack, the referenced data escapes to the heap.

  • Backing arrays of slices that get reallocated because an append would exceed their capacity. In cases where the initial size of a slice is known at compile time, it will begin its allocation on the stack. If this slice’s underlying storage must be expanded based on data only known at runtime, it will be allocated on the heap.

  • Calling methods on an interface type. Method calls on interface types are a dynamic dispatch — the actual concrete implementation to use is only determinable at runtime. Consider a variable r with an interface type of io.Reader. A call to r.Read(b) will cause both the value of r and the backing array of the byte slice b to escape and therefore be allocated on the heap.

form:Allocation efficiency in high-performance Go services

@itcuihao itcuihao changed the title fmt.Println 为什么会导致Go 变量逃逸? 关于 Go 逃逸分析 Mar 20, 2019
@itcuihao
Copy link
Owner Author

itcuihao commented Mar 20, 2019

来自上面的文章

It has been our experience that developers become proficient and productive in Go without understanding the performance characteristics of values versus pointers. A common hypothesis derived from intuition goes something like this: “copying values is expensive, so instead I’ll use a pointer.” However, in many cases copying a value is much less expensive than the overhead of using a pointer. “Why” you might ask?

难道用指针会比传值代价大吗?

package main

func main() {
	p := new(Person)
	p.New()
	p = p.Newp()
}

type Person struct {
	Name string
}

func (p *Person) New() {
	p.Name = "n"
	return
}
func (p *Person) Newp() *Person {
	p.Name = "p"
	return p
}

输出:

haoc7:~/mygo/src/tests$ go tool compile -m -l t1.go 
t1.go:13:7: (*Person).New p does not escape
t1.go:17:7: leaking param: p to result ~r0 level=0
t1.go:4:10: main new(Person) does not escape

line13 在之前的认知里,是会逃逸的啊,现在想来,原来逃逸是要看返回值的位置。

我们再来看一下,这三个方法的汇编代码:

New

"".(*Person).New STEXT size=89 args=0x8 locals=0x8
        0x0000 00000 (t1.go:15) TEXT    "".(*Person).New(SB), $8-8
        0x0000 00000 (t1.go:15) MOVQ    (TLS), CX
        0x0009 00009 (t1.go:15) CMPQ    SP, 16(CX)
        0x000d 00013 (t1.go:15) JLS     82
        0x000f 00015 (t1.go:15) SUBQ    $8, SP
        0x0013 00019 (t1.go:15) MOVQ    BP, (SP)
        0x0017 00023 (t1.go:15) LEAQ    (SP), BP
        0x001b 00027 (t1.go:15) FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
        0x001b 00027 (t1.go:15) FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x001b 00027 (t1.go:15) FUNCDATA        $3, gclocals·39825eea4be6e41a70480a53a624f97b(SB)
        0x001b 00027 (t1.go:16) PCDATA  $2, $1
        0x001b 00027 (t1.go:16) PCDATA  $0, $1
        0x001b 00027 (t1.go:16) MOVQ    "".p+16(SP), DI
        0x0020 00032 (t1.go:16) MOVQ    $1, 8(DI)
        0x0028 00040 (t1.go:16) PCDATA  $2, $-2
        0x0028 00040 (t1.go:16) PCDATA  $0, $-2
        0x0028 00040 (t1.go:16) CMPL    runtime.writeBarrier(SB), $0
        0x002f 00047 (t1.go:16) JNE     68
        0x0031 00049 (t1.go:16) LEAQ    go.string."n"(SB), AX
        0x0038 00056 (t1.go:16) MOVQ    AX, (DI)
        0x003b 00059 (t1.go:17) MOVQ    (SP), BP
        0x003f 00063 (t1.go:17) ADDQ    $8, SP
        0x0043 00067 (t1.go:17) RET
        0x0044 00068 (t1.go:16) LEAQ    go.string."n"(SB), AX
        0x004b 00075 (t1.go:16) CALL    runtime.gcWriteBarrier(SB)
        0x0050 00080 (t1.go:16) JMP     59
        0x0052 00082 (t1.go:16) NOP
        0x0052 00082 (t1.go:15) PCDATA  $0, $-1
        0x0052 00082 (t1.go:15) PCDATA  $2, $-1
        0x0052 00082 (t1.go:15) CALL    runtime.morestack_noctxt(SB)
        0x0057 00087 (t1.go:15) JMP     0

Newp

"".(*Person).Newp STEXT size=94 args=0x10 locals=0x8
        0x0000 00000 (t1.go:20) TEXT    "".(*Person).Newp(SB), $8-16
        0x0000 00000 (t1.go:20) MOVQ    (TLS), CX
        0x0009 00009 (t1.go:20) CMPQ    SP, 16(CX)
        0x000d 00013 (t1.go:20) JLS     87
        0x000f 00015 (t1.go:20) SUBQ    $8, SP
        0x0013 00019 (t1.go:20) MOVQ    BP, (SP)
        0x0017 00023 (t1.go:20) LEAQ    (SP), BP
        0x001b 00027 (t1.go:20) FUNCDATA        $0, gclocals·62420d0a7277934df9079483e4a3e39b(SB)
        0x001b 00027 (t1.go:20) FUNCDATA        $1, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
        0x001b 00027 (t1.go:20) FUNCDATA        $3, gclocals·39825eea4be6e41a70480a53a624f97b(SB)
        0x001b 00027 (t1.go:21) PCDATA  $2, $1
        0x001b 00027 (t1.go:21) PCDATA  $0, $1
        0x001b 00027 (t1.go:21) MOVQ    "".p+16(SP), DI
        0x0020 00032 (t1.go:21) MOVQ    $1, 8(DI)
        0x0028 00040 (t1.go:21) PCDATA  $2, $-2
        0x0028 00040 (t1.go:21) PCDATA  $0, $-2
        0x0028 00040 (t1.go:21) CMPL    runtime.writeBarrier(SB), $0
        0x002f 00047 (t1.go:21) JNE     73
        0x0031 00049 (t1.go:21) LEAQ    go.string."p"(SB), AX
        0x0038 00056 (t1.go:21) MOVQ    AX, (DI)
        0x003b 00059 (t1.go:22) PCDATA  $2, $0
        0x003b 00059 (t1.go:22) PCDATA  $0, $2
        0x003b 00059 (t1.go:22) MOVQ    DI, "".~r0+24(SP)
        0x0040 00064 (t1.go:22) MOVQ    (SP), BP
        0x0044 00068 (t1.go:22) ADDQ    $8, SP
        0x0048 00072 (t1.go:22) RET
        0x0049 00073 (t1.go:21) PCDATA  $2, $-2
        0x0049 00073 (t1.go:21) PCDATA  $0, $-2
        0x0049 00073 (t1.go:21) LEAQ    go.string."p"(SB), AX
        0x0050 00080 (t1.go:21) CALL    runtime.gcWriteBarrier(SB)
        0x0055 00085 (t1.go:21) JMP     59
        0x0057 00087 (t1.go:21) NOP
        0x0057 00087 (t1.go:20) PCDATA  $0, $-1
        0x0057 00087 (t1.go:20) PCDATA  $2, $-1
        0x0057 00087 (t1.go:20) CALL    runtime.morestack_noctxt(SB)
        0x005c 00092 (t1.go:20) JMP     0

Newnp

"".Newnp STEXT nosplit size=22 args=0x10 locals=0x0
        0x0000 00000 (t1.go:25) TEXT    "".Newnp(SB), NOSPLIT, $0-16
        0x0000 00000 (t1.go:25) FUNCDATA        $0, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x0000 00000 (t1.go:25) FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0000 00000 (t1.go:25) FUNCDATA        $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x0000 00000 (t1.go:26) PCDATA  $2, $1
        0x0000 00000 (t1.go:26) PCDATA  $0, $1
        0x0000 00000 (t1.go:26) LEAQ    go.string."n"(SB), AX
        0x0007 00007 (t1.go:26) PCDATA  $2, $0
        0x0007 00007 (t1.go:26) MOVQ    AX, "".~r0+8(SP)
        0x000c 00012 (t1.go:26) MOVQ    $1, "".~r0+16(SP)
        0x0015 00021 (t1.go:26) RET

NewNewp 必须用额外的代码,判断是否为0值

CMPL runtime.writeBarrier(SB), $0

Newnp则不需要额外的代码判断,直接赋值就可以。

但是我改如何对比此时,用指针与非指针参数的大小呢?

@Bai-Yingjie
Copy link

题主, 请问a为什么会逃逸到堆搞清楚了吗? a不是应该在栈上分配, 然后值拷贝给fmt.Println么? 想不通为什么会逃逸呢?

@Bai-Yingjie
Copy link

经过调查, 我现在认为fmt.Println(a)不会逃逸, 而fmt.Println(&a)会. a escapes to heap的打印有一定的迷惑性, 实际上, 如果出现moved to heap: a才是真正的逃逸. 可以反汇编搜索CALL runtime.newobject(SB)来确认是否确实逃逸了.

@simonhgao
Copy link

fmt.Println(&a)并不会触发逃逸
这种情况的根本是
在底层调用时doPrint 的reflect.TypeOf(arg).Kind() 导致的问题
但具体原因还不清楚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants