golang内存分配逃逸分析

逃逸分析在编译阶段完成,目的是决定内存分配地址是栈还是堆:编译时通过 go build -gcflags=-m 可以查看逃逸对象

参考:Go 逃逸分析 | Go 语言高性能编程 | 极客兔兔 (geektutu.com)

【转】Golang内存分配逃逸分析   -  叶落山城秋 (iphpt.com)

堆和栈

栈可以简单理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。

func F() {
	temp := make([]int, 0, 20)
	...
}

类似于上面代码里面的temp变量,只是内函数内部申请的临时变量,并不会作为返回值返回,它就是被编译器申请到栈里面

在栈上申请的内存 :函数返回直接释放,不会引起垃圾回收,对性能没有影响。

堆的情况·:

func F() []int{
	a := make([]int, 0, 20)
	return a
}

申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到 堆 上面了。

在堆上申请的对象生命周期可以超出函数调用的作用域,需要gc进行回收。如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。

看看这个:

func F() {
	a := make([]int, 0, 20)     // 栈 空间小
	b := make([]int, 0, 20000) // 堆 空间过大
 
	l := 20
	c := make([]int, 0, l) // 堆 动态分配不定空间
}

【转】Golang内存分配逃逸分析

一. golang 的内存分配逃逸 于堆和栈

注意我们此处谈到的堆和栈是对操作系统中的,这个和数据结构中的堆和栈还是又一定区别的。

1. 关于 堆和栈

栈 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。

func F() {
	temp := make([]int, 0, 20)
	...
}

类似于上面代码里面的temp变量,只是内函数内部申请的临时变量,并不会作为返回值返回,它就是被编译器申请到栈里面。

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

再来看看堆得情况之一如下代码:

func F() []int{
	a := make([]int, 0, 20)
	return a
}

而上面这段代码,申请的代码一模一样,但是申请后作为返回值返回了,编译器会认为变量之后还会被使用,当函数返回之后并不会将其内存归还,那么它就会被申请到 堆 上面了。

申请到堆上面的内存才会引起垃圾回收,如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。

我们再看看如下几个例子:

func F() {
	a := make([]int, 0, 20)     // 栈 空间小
	b := make([]int, 0, 20000) // 堆 空间过大
 
	l := 20
	c := make([]int, 0, l) // 堆 动态分配不定空间
}

像是 b 这种 即使是临时变量,申请过大也会在堆上面申请。

对于 c 编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。

逃逸分析·

在 C 语言中,可以使用 malloc 和 free 手动在堆上分配和回收内存。Go 语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定。那么,Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。

逃逸场景(分配到堆中)

 指针逃逸

指针逃逸应该是最容易理解的一种情况了,即在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。示例代码如下:

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("Jim", 18)
}

栈空间不足(空间开辟过大)

package main

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

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

func main() {
    Slice()
}

上面代码Slice()函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。 直接查看编译提示,如下:

所以只是1000的长度还不足以发生逃逸现象。然后就x10倍吧

package main

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

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

func main() {
    Slice()
}

当切片长度扩大到10000时就会逃逸。

实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

动态类型逃逸

在 Go 语言中,空接口即 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。

package main

import "fmt"

type Demo struct {
	name string
}

func createDemo(name string) *Demo {
	d := new(Demo) // 局部变量 d 逃逸到堆
	d.name = name
	return d
}

func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}

demo 是 main 函数中的一个局部变量,该变量作为实参传递给 fmt.Println(),但是因为 fmt.Println() 的参数类型定义为 interface{},因此也发生了逃逸。

fmt 包中的 Println 函数的定义如下:

func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

修改一下:

func test(demo *Demo) {
	fmt.Println(demo.name)
}

func main() {
	demo := createDemo("demo")
	test(demo)
}

这种情况下,局部变量 demo 不会发生逃逸,但是 demo.name 仍旧会逃逸。

闭包逃逸

func Increase() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	in := Increase()
	fmt.Println(in()) // 1
	fmt.Println(in()) // 2
}

Increase() 返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。

$ go build -gcflags=-m main_closure.go 
# command-line-arguments
./main_closure.go:6:2: moved to heap: n
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/src/gitHub/test/pool  go run main.go
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
Fibonacci: 55
~/go/src/gitHub/test/pool  go build -gcflags=-m
# gitHub/test/pool
./main.go:7:9: can inline Fibonacci.func1
./main.go:7:9: func literal escapes to heap
./main.go:7:9: func literal escapes to heap
./main.go:8:10: &b escapes to heap
./main.go:6:5: moved to heap: b
./main.go:8:13: &a escapes to heap
./main.go:6:2: moved to heap: a
./main.go:17:34: f() escapes to heap
./main.go:17:13: main ... argument does not escape

Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。

如何利用逃逸分析提升性能

传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,传递指针导致的 GC 开销可能会严重影响性能。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。

逃逸分析的作用

  • 逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。

  • 逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配 ,而没有发生逃逸的则有编译器在栈上分配)。

  • 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

逃逸总结:

  • 栈上分配内存比在堆中分配内存有更高的效率

  • 栈上分配的内存不需要GC处理

  • 堆上分配的内存使用完毕会交给GC处理

  • 逃逸分析目的是决定内分配地址是栈还是堆

  • 逃逸分析在编译阶段完成

在官网 (golang.org) FAQ 上有一个关于变量分配的问题,如下:

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.

如何得知变量是分配在栈(stack)上还是堆(heap)上?

准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。 然而,如果编译器不能确保变量在函数 return之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数return之后,变量不再被引用,则将其分配到栈上。

实际项目基本都是通过

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

来申请内存,长度都是不确定的,自然而然这些变量都会申请到堆上面了。

Golang使用的垃圾回收算法是『标记——清除』。

简单得说,就是程序要从操作系统申请一块比较大的内存,内存分成小块,通过链表链接。

每次程序申请内存,就从链表上面遍历每一小块,找到符合的就返回其地址,没有合适的就从操作系统再申请。如果申请内存次数较多,而且申请的大小不固定,就会引起内存碎片化的问题。

申请的堆内存并没有用完,但是用户申请的内存的时候却没有合适的空间提供。这样会遍历整个链表,还会继续向操作系统申请内存。这就能解释我一开始描述的问题,申请一块内存变成了慢语句。

为了解决以上问题引入 临时对象池SYNC.POOL

一句话总结:保存和复用临时对象,减少内存分配,降低 GC 压力。

Go 语言从 1.3 版本开始提供了对象重用的机制,即 sync.Pool。sync.Pool 是可伸缩的,同时也是并发安全的,其大小仅受限于内存的大小。sync.Pool 用于存储那些被分配了但是没有被使用,而未来可能会使用的值。这样就可以不用再次经过内存分配,可直接复用已有对象,减轻 GC 的压力,从而提升系统的性能。

sync.Pool 的大小是可伸缩的,高负载时会动态扩容,存放在池中的对象如果不活跃了会被自动清理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值