go语言的逃逸分析

go语言的逃逸分析

 次点击
18 分钟阅读

1、概述

  • 栈的作用域是函数、当函数执行结束后、栈的内存也会被销毁。堆的作用域通常是跨函数的

  • 当一个变量在函数外部被引用、此时需要把变量转移到堆上,我们一般说发生了逃逸

  • go语言中当访问一个引用对象时,实际是通过一个指针来间接访问、此时如果再访问里面的引用成员,往往会造成二次访问,这种操作很有可能会导致不必要的逃逸现象、因此我们在使用时,应尽量避免go语言的指针

  • 在 Go 语言中,由于使用了指针而导致变量出现不必要的逃逸,主要是因为编译器无法证明这些指针的生命周期局限于函数内部,因此将其分配到堆上。以下是所有常见的由于不正确使用指针导致不必要逃逸的情况:

2、案例

1. 指针引用局部变量

当指针指向一个局部变量时,Go 编译器会将局部变量分配到堆上,因为指针可能在函数返回后仍然被使用。

func foo() *int {
    x := 42       // x 逃逸到堆上,因为返回了指向 x 的指针
    return &x
}

优化方法:避免返回指针,直接返回值。

2. 将局部变量地址传递给外部函数

如果将局部变量的地址传递给外部函数,编译器无法确定该函数是否会保存该地址,因此会分配到堆上。

func bar(x *int) {
    fmt.Println(*x)
}

func foo() {
    x := 42       // x 逃逸到堆上,因为其地址传递给了 bar
    bar(&x)
}

优化方法:如果外部函数不需要持久化变量,传值而非指针。

3. 切片的底层数组逃逸

当指针指向切片的底层数组,且该数组可能会在函数外被修改时,数组可能逃逸。

func foo() []*int {
    nums := []int{1, 2, 3}
    ptrs := []*int{}
    for i := range nums {
        ptrs = append(ptrs, &nums[i]) // nums[i] 的地址逃逸
    }
    return ptrs
}

优化方法:避免将切片元素的地址存储到外部。

4. 结构体字段被取地址

当取结构体字段的地址时,字段可能逃逸到堆上,特别是当字段的地址被外部使用时。

type Point struct {
    X, Y int
}

func foo() *int {
    p := Point{1, 2}
    return &p.X    // p.X 逃逸,因为返回了其地址
}

优化方法:避免直接返回字段地址,优先考虑值传递。

5. 将指针作为接口参数传递

将指针传递给接口参数时,可能导致指针引用的变量逃逸。

func printValue(v interface{}) {
    fmt.Println(v)
}

func foo() {
    x := 42
    printValue(&x) // &x 逃逸,因为接口需要存储指针
}

优化方法:使用具体类型或传值而非指针。

6. 在 Goroutine 中使用指针

当局部变量的指针被 Goroutine 捕获时,编译器会将该变量分配到堆上。

func foo() {
    x := 42       // x 逃逸到堆上,因为 Goroutine 捕获了 x 的地址
    go func() {
        fmt.Println(x)
    }()
}

优化方法:避免在 Goroutine 中直接捕获局部变量,或使用值拷贝。

7. 指针指向数组或切片并在外部使用

数组或切片的地址被取出并传递到外部,导致底层数据逃逸。

func modify(arr *[3]int) {
    (*arr)[0] = 42
}

func foo() {
    nums := [3]int{1, 2, 3}
    modify(&nums) // nums 逃逸到堆上,因为其地址被传递
}

优化方法:在局部范围内直接操作数组或切片,避免传递指针。

8. 通过指针间接操作变量

当通过多级指针操作变量时,编译器通常会让变量逃逸到堆上。

func foo() **int {
    x := 42         // x 逃逸到堆上,因为通过二级指针返回
    px := &x
    return &px
}

优化方法:简化指针操作,避免多级指针。

9. 隐式指针导致的逃逸

某些情况下,隐式指针的使用也会导致逃逸,例如对字符串和切片的操作。

func appendValue(slice *[]int, value int) {
    *slice = append(*slice, value) // slice 底层数组可能逃逸
}

func foo() {
    nums := []int{}
    appendValue(&nums, 42)
}

优化方法:直接返回操作后的切片,避免传递切片指针。

总结

指针导致不必要逃逸的主要原因是 编译器无法证明变量的生命周期局限于函数内部。优化方法包括:

  1. 避免不必要的指针传递,优先考虑值传递。

  2. 减少对局部变量的地址操作。

  3. 避免在 Goroutine 中直接捕获局部变量。

  4. 明确变量的作用域,尽可能将其限制在函数内部。

通过遵循这些原则,可以减少不必要的堆分配,从而提升性能。

© 本文著作权归作者所有,未经许可不得转载使用。