• 指针逃逸


    GO语言变量逃逸分析

    空格键_11aa · 2019-07-09 21:32:41 · 274 次点击 · 预计阅读时间 1 分钟 · 大约1分钟之前 开始浏览

    这是一个创建于 2019-07-09 21:32:41 的文章,其中的信息可能已经有所发展或是发生改变。

    引言

    ​ 内存管理的灵活性是让C/C++程序猿们又爱又恨的东西,比如malloc或new一块内存我可以整个进程使用。但是,如果这块内存在某个函数中new了,但是暂时不能释放那就是悲剧开始了。鬼知道何时释放合适及是不是我还记得我new过它。所以后来很多语言都限制了内存管理或者优化了内存管理机制,添加gc机制来“辅助”程序猿们编程。变量分配在堆上还是栈上不是由是否new/malloc决定,而是通过编译器的“逃逸分析”来决定。

    什么是逃逸分析

    ​ 在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。也是就是说逃逸分析是解决指针作用范围的编译优化方法。编程中常见的两种逃逸情景:

    ​ 1,函数中局部对象指针被返回(不确定被谁访问)

    ​ 2,对象指针被多个子程序(如线程 协程)共享使用

    为什么要做逃逸分析

    ​ 开始我们提到go语言中对象内存的分配不是由语言运算符或函数决定,而是通过逃逸分析来决定。为什么要这么干呢?其实说到底还是为了优化程序。函数中生成一个新对象:

    1,如果分配到栈上,待函数返回资源就被回收了

    2,如果分配到堆上,函数返回后交给gc来管理该对象资源

    栈资源的分配及回收速度比堆要快,所以逃逸分析最大的好处应该是减少了GC的压力。

    逃逸分析原理

    逃逸分析的场景

    指针逃逸

    典型的逃逸case,函数返回局部变量的指针。

    img

    运行:go build -gcflags "-m -l" escap01.go

    -m 可以用多个来打印更详细的信息,-l去掉inline信息。局部变量a被分配到堆上。

    栈空间不足逃逸

    当对象大小超过的栈帧大小时(详见go内存分配),变量对象发生逃逸被分配到堆上。

    img

    当s的容量足够大时,s逃逸到堆上。t容量较小分配到栈上

    闭包引用逃逸

    img

    Fibonacci()函数返回一个函数变量赋值给f,f就成了一个闭包。闭包f保存了a b的地址引用,所以每次调用f()后ab的值发生变化。ab发生逃逸。

    img

    但如果直接调用Fibonacci(),则ab都是独立的局部变量。

    动态类型逃逸

    当对象不确定大小或者被作为不确定大小的参数时发生逃逸。

    img

    t的大小是个变量所以会逃逸到堆上。size作为interface{}参数逃逸到堆上。

    切片或map赋值

    在给切片或者map赋值对象指针(与对象共享内存地址时),对象会逃逸到堆上。但赋值对象值或者返回对象值切片是不会发生逃逸的。

    img

    变量逃逸情况还有很多,暂时学习整理这些。程序性能优化是一个很重要的方向,对于现在还在完善的go编译器,我们需要不断总结现有缺陷,尽量在编码时注意潜在的问题,不要把优化都留给编译器(也不可能都留给它,因为我也不知道要优化什么 0-0 )。

    逃逸策略

    • 如果编译器不能证明某个变量在函数返回后不再被引用,则分配在堆上
    • 如果一个变量过大,则有可能分配在堆上

    分析目的

    • 不逃逸的对象分配在栈上,则变量在用完后就会被编译器回收,从而减少GC的压力
    • 栈上的分配要比堆上的分配更加高效
    • 同步消除,如果定义的对象上有同步锁,但是栈在运行时只有一个线程访问,逃逸分析后如果在栈上则会将同步锁去除

    逃逸场景

    指针逃逸

    在 build 的时候,通过添加 -gcflags "-m" 编译参数就可以查看编译过程中的逃逸分析

    在有些时候,因为变量太大等原因,我们会选择返回变量的指针,而非变量,这里其实就是逃逸的一个经典现象

    func main() {
        test()
    }
    
    func test() *int {
        i := 1
        return &i
    }

    逃逸分析结果:

    # command-line-arguments
    ./main.go:7:6: can inline test
    ./main.go:3:6: can inline main
    ./main.go:4:6: inlining call to test
    ./main.go:4:6: main &i does not escape
    ./main.go:9:9: &i escapes to heap
    ./main.go:8:2: moved to heap: i

    可以看到最后两行指出,变量 i 逃逸到了 heap

    栈空间不足逃逸

    首先,我们尝试创建一个 长度较小的 slice

    func main() {
        stack()
    }
    
    func stack() {
        s := make([]int, 10, 10)
        s[0] = 1
    }

    逃逸分析结果:

    ./main.go:12:6: can inline stack
    ./main.go:3:6: can inline main
    ./main.go:4:7: inlining call to stack
    ./main.go:4:7: main make([]int, 10, 10) does not escape
    ./main.go:13:11: stack make([]int, 10, 10) does not escape

    结果显示未逃逸

    然后,我们创建一个超大的slice

    func main() {
        stack()
    }
    
    func stack() {
        s := make([]int, 100000, 100000)
        s[0] = 1
    }

    逃逸分析结果:

    ./main.go:12:6: can inline stack
    ./main.go:3:6: can inline main
    ./main.go:4:7: inlining call to stack
    ./main.go:4:7: make([]int, 100000, 100000) escapes to heap
    ./main.go:13:11: make([]int, 100000, 100000) escapes to heap

    这时候就逃逸到了堆上了

    动态类型逃逸

    func main() {
        dynamic()
    }
    
    func dynamic() interface{} {
        i := 0
        return i
    }

    逃逸分析结果:

    ./main.go:18:6: can inline dynamic
    ./main.go:3:6: can inline main
    ./main.go:5:9: inlining call to dynamic
    ./main.go:5:9: main i does not escape
    ./main.go:20:2: i escapes to heap

    这里的动态类型逃逸,其实在理解了interface{}的内部结构后,还是可以归并到 指针逃逸 这一类的,有兴趣的同学可以看一下 《深入理解Go的interface》

    闭包引用逃逸

    func main() {
        f := fibonacci()
        for i := 0; i < 10; i++ {
            f()
        }
    }
    func fibonacci() func() int {
        a, b := 0, 1
        return func() int {
            a, b = b, a+b
            return a
        }
    }

    逃逸分析结果:

    ./main.go:11:9: can inline fibonacci.func1
    ./main.go:11:9: func literal escapes to heap
    ./main.go:11:9: func literal escapes to heap
    ./main.go:12:10: &b escapes to heap
    ./main.go:10:5: moved to heap: b
    ./main.go:12:13: &a escapes to heap
    ./main.go:10:2: moved to heap: a

    逃逸的其他情况

    1.被已经逃逸的变量引用的指针,肯定会引发逃逸

    type A struct{
        data *int
    }
    
    func newA() *A{
        return &A{}
    }
    func main(){
        a := newA() //返回值是A结构体指针
        b := 2
        a.data = &b //此时a的指针必然发生逃逸
    }
    

    下图可以看到A是一个已经逃逸的指针,引用了b的指针,所以b的指针也发生了逃逸

    image-20191216161147822

    我们再看上面备注中的代码例子:

    func main() {
        a := make([]*int,1)
        b := 12
        a[0] = &b
    }

    结果:

    ➜  testProj go run -gcflags "-m -l" main.go
    # command-line-arguments
    ./main.go:7:2: moved to heap: b
    ./main.go:6:11: main make([]*int, 1) does not escape

    sliace a并没有发生逃逸,但是被a引用的b依然逃逸了。类似的情况同样发生在map和chan中:

    func main() {
        a := make([]*int,1)
        b := 12
        a[0] = &b
    
        c := make(map[string]*int)
        d := 14
        c["aaa"]=&d
    
        e := make(chan *int,1)
        f := 15
        e <- &f
    }

    结果:

    ➜  testProj go run -gcflags "-m -l" main.go
    # command-line-arguments
    ./main.go:7:2: moved to heap: b
    ./main.go:11:2: moved to heap: d
    ./main.go:15:2: moved to heap: f
    ./main.go:6:11: main make([]*int, 1) does not escape
    ./main.go:10:11: main make(map[string]*int) does not escape

    被chan,map,slice引用的指针必然会发生逃逸

    image-20191216161942763

    由此我们可以得出结论:

    被指针类型的slice、map和chan引用的指针一定发生逃逸
    备注:stack overflow上有人提问为什么使用指针的chan比使用值的chan慢30%,答案就在这里:使用指针的chan发生逃逸,gc拖慢了速度。问题链接https://stackoverflow.com/questions/41178729/why-passing-pointers-to-channel-is-slower

    总结

    我们得出了指针必然发生逃逸的三种情况(go version go1.13.4 darwin/amd64):

    • 在某个函数中new或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸(构造函数返回的指针变量一定逃逸);
    • 被已经逃逸的变量引用的指针,一定发生逃逸;
    • 被指针类型的slice、map和chan引用的指针,一定发生逃逸;

    同时我们也得出一些必然不会逃逸的情况:

    • 指针被未发生逃逸的变量引用;
    • 仅仅在函数内对变量做取址操作,而未将指针传出;

    **有一些情况****可能发生逃逸,也可能不会发生逃逸:

    • 将指针作为入参传给别的函数;这里还是要看指针在被传入的函数中的处理过程,如果发生了上边的三种情况,则会逃逸;否则不会逃逸;




  • 相关阅读:
    使用数据(二)
    lambda表达式
    方法引用::
    开发 Web 应用(一)
    Spring基础(三)
    Spring基础(二)
    Spring 基础(一)
    项目实践之Ajax 技术使用教程
    项目实践之前后端分离详解
    考研计算机基础:构造算法与自上而下逐步完善:实例研究3(嵌套控制结构)
  • 原文地址:https://www.cnblogs.com/hualou/p/12069815.html
Copyright © 2020-2023  润新知