关于后端:golang逃逸技术分析

40次阅读

共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。

申请到栈内存益处:函数返回间接开释,不会引起垃圾回收,对性能没有影响。

申请到堆下面的内存才会引起垃圾回收。

func F() {a := make([]int, 0, 20)
    b := make([]int, 0, 20000)

    l := 20
    c := make([]int, 0, l)
}

a 和 b 代码一样,就是申请的空间不一样大,然而它们两个的命运是截然相同的。a 后面曾经介绍过,会申请到栈下面,而 b,因为申请的内存较大,编译器会把这种申请内存较大的变量转移到堆下面。即便是长期变量,申请过大也会在堆下面申请。

而 c,对咱们而言其含意和 a 是统一的,然而编译器对于这种不定长度的申请形式,也会在堆下面申请,即便申请的长度很短。

<font color=”#FF69B4″> 堆 (Heap) 和栈(Stack)</font>

参考 此文 < 内存模型:Heap>, < 内存模型:Stack> 局部的内容:

Heap:

堆的一个重要特点就是不会主动隐没,必须手动开释,或者由垃圾回收机制来回收。

Stack:

栈是因为函数运行而长期占用的内存区域

执行 main 函数时,会为它在内存外面建设一个帧(frame),所有 main 的外部变量(比方 a 和 b)都保留在这个帧外面。main 函数执行完结后,该帧就会被回收,开释所有的外部变量,不再占用空间。

一般来说,调用栈有多少层,就有多少帧。

所有的帧都寄存在 Stack,因为帧是一层层叠加的,所以 Stack 被翻译为 。(栈这个字的原始含意, 就有栅栏的意思, 所谓 栈道, 栈桥, 都是指比拟简陋的用栅栏做的路线 / 桥梁)

即 在函数中申请一个新的对象:

如果调配 在栈中,则函数执行完结可主动将内存回收;不会引起垃圾回收,对性能没有影响。

如果调配在堆中,则函数执行完结可交给 GC(垃圾回收)解决; 如果这个过程(特指垃圾回收一直被触发)过于高频就会导致 gc 压力过大,程序性能出问题。

C/C++ 中的 new 都是调配到堆上,Go 则不肯定(Java 亦然)”)


<font color=”#FF69B4″> 何为逃逸剖析(Escape analysis)</font>

在堆上调配的内存, 须要 GC 去回收, 而在栈上调配, 函数执行完就销毁, 不存在垃圾回收的问题. 所以应尽可能将内存调配在栈上.

但问题是, 对于一个函数或变量, 并不能晓得还有没有其余中央在援用. 所谓的逃逸剖析, 就是为了确定这个事儿~

<p class=”pbgr”>Go 编译器会逾越函数和包的边界进行全局的逃逸剖析。它会查看是否须要在堆上为一个变量分配内存,还是说能够在栈自身的内存里对其进行治理。</p>


<font color=”#FF69B4″> 何时产生逃逸剖析?</font>

Go 编译器决定变量应该调配到什么中央时会进行逃逸剖析

From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.

However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

<div style=”color:#708090;background=#F5FFFA”>

Q:如何得悉变量是调配在栈(stack)上还是堆(heap)上?

A: 精确地说,你并不需要晓得。Golang 中的变量只有被援用就始终会存活,存储在堆上还是栈上由外部实现决定而和具体的语法没有关系。

但晓得变量的存储地位的确对程序的效率有帮忙。如果可能,Golang 编译器会将函数的局部变量调配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被援用,编译器就会将变量调配到堆上。而且,如果一个局部变量十分大,那么它也应该被调配到堆上而不是栈上。 当前情况下,如果一个变量被取地址,那么它就有可能被调配到堆上。然而,还要对这些变量做逃逸剖析,如果函数 return 之后,变量不再被援用,则将其调配到栈上。

</div>

能够应用 go 命令的 -gcflags="-m"选项,来察看逃逸剖析的后果以及 GC 工具链的内联决策 ([内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体自身。这么做的起因是它能够打消函数调用自身的开销,也使得编译器能更高效地执行其余的优化策略。咱们能够显式地在函数定义后面加一行//go:noinline 正文让编译器不对函数进行内联)


<font color=”#FF69B4″> 实例 </font>

对于 escape1.go 代码如下:

package main

import "fmt"

func main() {fmt.Println("Called stackAnalysis", stackAnalysis())
}

//go:noinline
func stackAnalysis() int {
    data := 100
    return data
}

通过 go build -gcflags "-m -l" escape1.go go build -gcflags=-m escape1.go 来查看和剖析逃逸剖析:

./escape1.go:6:13: inlining call to fmt.Println
./escape1.go:6:14: "Called stackAnalysis" escapes to heap
./escape1.go:6:51: stackAnalysis() escapes to heap
./escape1.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

escapes to heap 即代表该行该处 内存调配产生了逃逸景象. 变量须要在函数栈之间共享(这个例子就是在 main 和 fmt.Println 之间在栈上共享)

  • 第 6 行第 13 个字符处的字符串标量 ”Called stackAnalysis” 逃逸到堆上
  • 第 6 行 51 个字符处的函数调用 stackAnalysis()逃逸到了堆上

对于 escape2.go 代码如下:

package main

import "fmt"

func main() {fmt.Println("Called heapAnalysis", heapAnalysis())
}

//go:noinline
func heapAnalysis() *int {
    data := 100
    return &data
}

执行go build -gcflags=-m escape2.go:

# command-line-arguments
./escape2.go:6:13: inlining call to fmt.Println
./escape2.go:11:2: moved to heap: data
./escape2.go:6:14: "Called heapAnalysis" escapes to heap
./escape2.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

函数 heapAnalysis 返回 int 类型的指针, 在 main 函数中会应用该指针变量. 因为是在 heapAnalysis 函数内部拜访, 所以 data 变量必须被挪动到堆上

主函数 main 会从堆中拜访该 data 变量

(可见指针虽可能缩小变量在函数间传递时的数据值拷贝, 但不该所有类型数据都返回其指针. 如果调配到堆上的共享变量太多会减少了 GC 的压力)


<font color=”#FF69B4″> 逃逸类型 </font>

<font color=”#00FFFF”>1. 指针逃逸:</font>

对于 escape_a.go:

package main

type Student struct {
    Name string
    Age  int
}

func StudentRegister(name string, age int) *Student {s := new(Student) // 局部变量 s 逃逸到堆

    s.Name = name
    s.Age = age

    return s
}

func main() {StudentRegister("dashen", 18)
}

执行 go build -gcflags=-m escape_a.go

# command-line-arguments
./escape_a.go:8:6: can inline StudentRegister
./escape_a.go:17:6: can inline main
./escape_a.go:18:17: inlining call to StudentRegister
./escape_a.go:8:22: leaking param: name
./escape_a.go:9:10: new(Student) escapes to heap
./escape_a.go:18:17: new(Student) does not escape

s 尽管为 函数 StudentRegister()内的局部变量, 其值通过函数返回值返回. 但 s 自身为指针类型. 所以其指向的内存地址不会是栈而是堆.

这是一种典型的变量逃逸案例

<font color=”#00FFFF”>2. 栈空间有余而导致的逃逸(空间开拓过大):</font>

对于 escape_b.go:

package main

func InitSlice() {s := make([]int, 1000, 1000)

    for index := range s {s[index] = index
    }
}

func main() {InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:11:6: can inline main
./escape_b.go:4:11: make([]int, 1000, 1000) does not escape

此时并没有产生逃逸

将切片的容量增大 10 倍, 即:

package main

func InitSlice() {s := make([]int, 1000, 10000)

    for index := range s {s[index] = index
    }
}

func main() {InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:11:6: can inline main
./escape_b.go:4:11: make([]int, 1000, 10000) escapes to heap

产生了逃逸

当栈空间不足以寄存以后对象, 或无奈判断以后切片长度时, 会将对象调配到堆中

ps:

package main

func InitSlice() {s := make([]int, 1000, 1000)

    for index := range s {s[index] = index
    }
    println(s)
}

func main() {InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:12:6: can inline main
./escape_b.go:4:11: make([]int, 1000, 1000) does not escape

没有逃逸.

而改成

package main

import "fmt"

func InitSlice() {s := make([]int, 1000, 1000)

    for index := range s {s[index] = index
    }
    fmt.Println(s)
}

func main() {InitSlice()
}

执行go build -gcflags=-m escape_b.go, 则

# command-line-arguments
./escape_b.go:11:13: inlining call to fmt.Println
./escape_b.go:14:6: can inline main
./escape_b.go:6:11: make([]int, 1000, 1000) escapes to heap
./escape_b.go:11:13: s escapes to heap
./escape_b.go:11:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

产生了逃逸

这是为何? 参见下文!

<font color=”#00FFFF”>3. 动静类型逃逸(不确定长度大小):</font>

当函数参数为 interface{} 类型, 如最罕用的fmt.Println(a …interface{}), 编译期间很难确定其参数的具体类型, 也会产生逃逸

对于 escape_c1.go:

package main

import "fmt"

func main() {
    s := "s 会产生逃逸"
    fmt.Println(s)
}

执行go build -gcflags=-m escape_c1.go

# command-line-arguments
./escape_c1.go:7:13: inlining call to fmt.Println
./escape_c1.go:7:13: s escapes to heap
./escape_c1.go:7:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

对于 escape_c1.go:

package main

func main() {InitSlice2()
}

func InitSlice2() {a := make([]int, 0, 20)    // 栈 空间小
    b := make([]int, 0, 20000) // 堆 空间过大 逃逸

    l := 20
    c := make([]int, 0, l) // 堆 动态分配不定空间 逃逸

    _, _, _ = a, b, c
}

执行go build -gcflags=-m escape_c2.go

# command-line-arguments
./escape_c2.go:7:6: can inline InitSlice2
./escape_c2.go:3:6: can inline main
./escape_c2.go:4:12: inlining call to InitSlice2
./escape_c2.go:4:12: make([]int, 0, 20) does not escape
./escape_c2.go:4:12: make([]int, 0, 20000) escapes to heap
./escape_c2.go:4:12: make([]int, 0, l) escapes to heap
./escape_c2.go:8:11: make([]int, 0, 20) does not escape
./escape_c2.go:9:11: make([]int, 0, 20000) escapes to heap
./escape_c2.go:12:11: make([]int, 0, l) escapes to heap

<font color=”#00FFFF”>4. 闭包援用对象逃逸:</font>

对于如下斐波那契数列escape_d.go:

package main

import "fmt"

func Fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

func main() {f := Fibonacci()

    for i := 0; i < 10; i++ {fmt.Printf("Fibonacci: %d\n", f())
    }
}

执行go build -gcflags=-m escape_d.go

# command-line-arguments
./escape_d.go:7:9: can inline Fibonacci.func1
./escape_d.go:17:13: inlining call to fmt.Printf
./escape_d.go:6:2: moved to heap: a
./escape_d.go:6:5: moved to heap: b
./escape_d.go:7:9: func literal escapes to heap
./escape_d.go:17:34: f() escapes to heap
./escape_d.go:17:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

Fibonacci()函数 中本来属于局部变量的 a 和 b, 因为闭包的援用, 不得不将二者放到堆上, 从而产生逃逸


<font color=”#FF69B4″> 总结 </font>

  • 逃逸剖析在编译阶段实现
  • 逃逸剖析目标是决定内调配地址是栈还是堆
  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上调配的内存不须要 GC 解决
  • 堆上调配的内存应用结束会交给 GC 解决

通过逃逸剖析, 不逃逸的对象调配在栈上, 当函数返回时就回收了资源, 不需 gc 标记革除, 从而缩小 gc 的压力

同时, 栈的调配比堆快, 性能好(逃逸的局部变量会在堆上调配, 而没有产生逃逸的则有编译器在栈上调配)

另外, 还能够进行 同步打消: 如果定义的对象的办法上有同步锁, 但在运行时却只有一个线程在拜访, 此时逃逸剖析后的机器码会去掉同步锁运行


全文参考自:

Go 内存治理之代码的逃逸剖析

Golang 内存调配逃逸剖析

举荐浏览:

golang 如何优化编译、逃逸剖析、内联优化

java 逃逸技术剖析

译文 Go 高性能系列教程之三:编译器优化

本文由 mdnice 多平台公布

正文完
 0