golang逃逸分析

1、堆内存和栈内存是什么

栈内存上的对象的存储空间是自动分配和销毁的,无需开发人员或编程语言运行时过多参与(作用域函数内);

内存对象,可以在全局(跨函数间)合法使用,这就是堆内存对象,堆内存对象需要通过专用API手工分配和释放,在C中对应的分配和释放方法就是malloc和free;

c语言程序解释

#include <stdio.h>
#include <stdlib.h>

int *foo() {
 int *c = malloc(sizeof(int));
 *c = 12;
 return c;
}

int main() {
 int *p = foo();
 printf("the return value of foo = %d\n", *p);
 free(p);
}

 

堆内存对象的生命周期管理将会给开发人员带来很大的心智负担。为了降低这方面的心智负担,带有GC(垃圾回收)的编程语言出现了,比如Java、Go等。这些带有GC的编程语言会对位于堆上的对象进行自动管理。当某个对象不可达时(即没有其对象引用它时),它将会被回收并被重用。

但GC的出现虽然降低了开发人员在内存管理方面的心智负担,但GC不是免费的,它给程序带来的性能损耗是不可忽视的,尤其是当堆内存上有大量待扫描的堆内存对象时,将会给GC带来过大的压力,从而使得GC占用更多本应用于处理业务逻辑的计算和存储资源。于是人们开始想方法尽量减少在堆上的内存分配,可以在栈上分配的变量尽量留在栈上。

 

2、逃逸分析是什么

 

逃逸分析(escape analysis)就是在程序编译阶段根据程序代码中的数据流,对代码中哪些变量需要在栈上分配,哪些变量需要在堆上分配进行静态分析的方法

 

作用是:

帮助程序员将那些人们认为需要分配在栈上的变量尽可能保留在栈上,尽可能少的“逃逸”到堆上的算法

 

3、对int和slice做逃逸分析 - 如何减少变量逃逸提升程序性能

a、int 实例:

package main

import "testing"

// 逃逸分析(escape analysis)就是在程序编译阶段根据程序代码中的数据流,对代码中哪些变量需要在栈上分配,
// 哪些变量需要在堆上分配进行静态分析的方法

// 位于栈上的内存对象由程序自行创建销毁不同,堆内存对象需要通过专用API手工分配和释放,在C中对应的分配和释放方法就是malloc和free

// 尽量减少在堆上的内存分配,可以在栈上分配的变量尽量留在栈上

// 而函数foo中的a以及指针p指向的内存块都在栈上分配(即便我们是调用的new创建的int对象,
// Go中new出来的对象可不一定分配在堆上,逃逸分析的输出日志中还专门提及new(int)没有逃逸)
// 未逃逸的a和p指向的内存块的地址区域在0xc000074860~0xc000074868
func foo() {
    // 整型
    a := 11
    // 指针
    p := new(int)
    *p = 12
    // 未逃逸
    println("addr of a is", &a)
    // 未逃逸
    println("addr that p point to is", p)
}

// 逃逸的m和n被分配到了堆内存空间,从输出的结果来看在0xc0000160e0~0xc0000160e8
// 函数bar中执行了两次堆内存分配动作

// 源码的14和15行,汇编调用了runtime.newobject在堆上执行了内存分配动作,这恰是逃逸的m和n声明的位置。
// 实际上在gc管理的内存上执行了malloc动作
func bar() (*int, *int) {
    m := 21
    n := 22
    println("addr of m is", &m)
    println("addr of n is", &n)
    // 返回栈内存变量的指针
    // bar中的m、n逃逸到heap
    // 这两个变量将在heap上被分配存储空间
    return &m, &n
}

func main() {
    // go build -gcflags "-m -l" int.go
    // go run -gcflags "-l" int.go
    // 逃逸分析 - 栈内存变量逃逸堆内存变量 - 叫做逃逸

    println(int(testing.AllocsPerRun(1, foo)))
    println(int(testing.AllocsPerRun(1, func() {
        bar()
    })))
}

 

b、slice 示例

package main

import (
    "reflect"
    "unsafe"
)

// slice的原理:切片实现原理 - 三元组
//type slice struct {
//    array unsafe.Pointer
//    len   int
//    cap   int
//}

// 声明了一个空slice
// slice自身是分配在栈上的,但是运行时在动态扩展切片时,选择了将其元素存储在heap上
func noEscapeSliceWithDataInHeap() {
    var sl []int
    println("addr of local(noescape, data in heap) slice = ", &sl)
    printSliceHeader(&sl)
    sl = append(sl, 1)
    println("append 1")
    printSliceHeader(&sl)
    println("append 2")
    sl = append(sl, 2)
    printSliceHeader(&sl)
    println("append 3")
    sl = append(sl, 3)
    printSliceHeader(&sl)
    println("append 4")
    sl = append(sl, 4)
    printSliceHeader(&sl)
}

// noEscapeWithDataInStack直接初始化了一个包含8个元素存储空间的切片,切片自身没有逃逸,
//并且在附加(append)的元素个数小于等于8个的时候,元素直接使用了为其分配的栈空间

// 如果附加的元素超过8个,那么运行时会在堆上分配一个更大的空间并将原栈上的8个元素复制过去,后续该切片的元素就都存储在了堆上

// 为什么强烈建议在创建 slice 时带上预估的cap参数的原因: 减少了堆内存的频繁分配、cap容量之下,所有元素都分配在栈上
func noEscapeSliceWithDataInStack() {
    var sl = make([]int, 0, 8)
    println("addr of local(noescape, data in stack) slice = ", &sl)
    printSliceHeader(&sl)
    sl = append(sl, 1)
    println("append 1")
    printSliceHeader(&sl)
    sl = append(sl, 2)
    println("append 2")
    printSliceHeader(&sl)
}

// escapeSlice则是切片变量自身以及其元素的存储都在堆上
func escapeSlice() *[]int {
    var sl = make([]int, 0, 8)
    println("addr of local(escape) slice = ", &sl)
    printSliceHeader(&sl)
    sl = append(sl, 1)
    println("append 1")
    printSliceHeader(&sl)
    sl = append(sl, 2)
    println("append 2")
    printSliceHeader(&sl)
    return &sl
}

func printSliceHeader(p *[]int) {
    ph := (*reflect.SliceHeader)(unsafe.Pointer(p))
    println("slice data =", unsafe.Pointer(ph.Data))
}

func main() {
    noEscapeSliceWithDataInHeap()
    noEscapeSliceWithDataInStack()
    escapeSlice()
}

 

所以以上结论就是 - 最好栈内存对象不要指针向外抛出导致内存逃逸,slice 尽量创建时候分配上 cap 参数(容量) - 还有就是 make slice 的时候指定初始长度 0 的意义

 

转载自:

https://mp.weixin.qq.com/s/ALzzT1kQ3VfYQly1LT7mQw

https://www.zhihu.com/question/440402836

 作者:无忌

链接:https://www.zhihu.com/question/440402836/answer/2600865265
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Go官方给的说法是:从程序正确性的角度而言,你不需要关心变量是分配在stack上还是heap上。变量分配在哪块内存空间不改变Go语言的语义。从程序性能的角度而言,
你可以关心变量到底是分配在stack上还是heap上,因为正如上文所言,变量存储的位置是对性能有影响的。

How
do I know whether a variable is allocated on the heap or the stack?
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.

一般而言,遇到以下情况会发生逃逸行为,Go编译器会将变量存储在heap上

  • 函数内局部变量在函数外部被引用
  • 接口(interface)类型的变量
  • size未知或者动态变化的变量,如slice,map,channel,[]byte等
  • size过大的局部变量,因为stack内存空间比较小。

此外,我们还可以借助内存逃逸分析工具来帮助我们。

因为内存逃逸分析是编译器在编译期就完成的,可以使用以编译下命令来做内存逃逸分析:

  • go build -gcflags="-m",可以展示逃逸分析、内联优化等各种优化结果。
  • go build -gcflags="-m -l"-l会禁用内联优化,这样可以过滤掉内联优化的结果展示,让我们可以关注逃逸分析的结果。
  • go build -gcflags="-m -m",多一个-m会展示更详细的分析结果。


作者:无忌
链接:https://www.zhihu.com/question/440402836/answer/2600865265
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

 

posted @ 2022-06-18 13:11  许伟强  阅读(198)  评论(0编辑  收藏  举报