golang逃逸分析

带GC语言给我们程序的编写带来了极大的便利,但是与此同时屏蔽了很多底层的细节,比如一个对象是在栈上分配还是在堆上分配。对于普通的代码来说虽然不需要关心这么多,但是作为强迫症程序猿,还是希望能让自己写出来的代码性能最优,所以还是需要了解什么是逃逸,以及如何判断是否发生了逃逸。

什么是堆和栈?

首先需要知道,我们说的堆和栈是啥。这个可不是数据结构里面的"堆"和"栈",而是操作系统里面的概念。

在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式,大小在编译时已经确定,寻址起来也十分迅速,开销很少。这一块内存地址称为栈。栈是线程级别的,大小在创建的时候已经确定,所以当数据太大的时候,就会发生"stack overflow"。

在程序中,全局变量、内存占用大的局部变量、发生了逃逸的局部变量存在的地方就是堆,这一块内存没有特定的结构,也没有固定的大小,可以根据需要进行调整。简单来说,有大量数据要存的时候,就存在堆里面。堆是进程级别的。当一个变量需要分配在堆上的时候,开销会比较大,对于go这种带GC的语言来说,也会增加gc压力,同时也容易造成内存碎片。

为什么有的变量要分配在堆,有的要分配在栈?

这个问题要从C++说起了。在C++中,假设我们有以下代码:

```c++
int* f1() {
int i = 5;
return &i;
}

int main() {
int i = f1();
i = 6;
return 0;
}


这时候程序结果是无法预期的,因为在函数f1中,i是一个局部变量,会分配在栈上,而栈在函数返回之后就失效了(Plan9 汇编中SP指针被修改),于是i的地址所存的值是不可预期的,后续在main中对返回的i的地址中的值的修改可能会修改掉程序运行的数据,造成结果无法预期。

所以对于需要返回一个地址回去的情况,在C++中需要用new来分配一块堆上的内存才行,因为堆是进程级别的,也就是全局的,除非程序猿手动释放,否则不会被回收(释放不好会段错误,忘了释放会内存泄漏),于是就可以使得这个地址不会再被使用到,可以安全地返回。

## 如何进行逃逸分析?

在golang中,所有内存都是由runtime管理的,程序猿不需要关心具体变量分配在哪里,什么时候回收,但是编译器需要知道这一点,这样才能确定函数栈帧大小、哪些变量需要"new"在堆上,所以编译器需要进行`逃逸分析`。简单来说,`逃逸分析`决定了一个变量是分配在栈上还是分配在堆上。

golang逃逸分析最基本的原则是:`如果一个函数返回的是一个(局部)变量的地址,那么这个变量就发生逃逸`。

在golang里面,变量分配在何处和是否使用new无关,意味着程序猿无法手动指定某个变量必须分配在栈上或者堆上(自己撸asm的当我没说),所以我们需要通过一些方法来确定某个变量到底是分配在了栈上还是堆上。

我们用以下代码作为例子:

```go
package main

func main() {
    a := f1()
    *a++
}

//go:noinline
func f1() *int {
    i := 1
    return &i
}

在以上代码中,给f1增加了noinline标记,让go编译器不要将函数内联。

使用编译参数

golang提供了编译的参数让我们可以直观地看到变量是否发生了逃逸,只需要在go build时指定 -gcflags '-m'即可:

$ go build -gcflags '-m' escape.go
# command-line-arguments
./escape.go:3:6: can inline main
./escape.go:11:9: &i escapes to heap
./escape.go:10:2: moved to heap: i

这样可以很直观地看到在第10、11行,i发生了逃逸,内存会分配在堆上。

除了使用编译参数之外,我们还可以使用一种更底层的,更硬核,也更准确的方式来判断一个对象是否逃逸,那就是:直接看汇编!

使用汇编

我们使用go tool compile -S生成汇编代码:

$ go tool compile -S escape.go | grep escape.go:10
    0x001d 00029 (escape.go:10) PCDATA  $2, $1
    0x001d 00029 (escape.go:10) PCDATA  $0, $0
    0x001d 00029 (escape.go:10) LEAQ    type.int(SB), AX
    0x0024 00036 (escape.go:10) PCDATA  $2, $0
    0x0024 00036 (escape.go:10) MOVQ    AX, (SP)
    0x0028 00040 (escape.go:10) CALL    runtime.newobject(SB)
    0x002d 00045 (escape.go:10) PCDATA  $2, $1
    0x002d 00045 (escape.go:10) MOVQ    8(SP), AX
    0x0032 00050 (escape.go:10) MOVQ    $1, (AX)

可以看到,这里的00040有调用runtime.newobject(SB)这个方法,看到这个方法大家就应该懂了!

总结

以上提供了两种方法可以用来判断某个变量是否发生了逃逸,其中使用编译参数比较简单,使用汇编比较硬核。通过这两种方法分析完逃逸,就能进一步优化堆上内存数量,减轻GC压力了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Go语言中,内存逃逸指的是当一个对象的指针被多个方法或线程引用时,这个指针会逃逸到堆上。内存逃逸的位置由编译器决定,而不像C或C++可以使用malloc或new在堆上分配内存。根据内存分配的基本原则,当函数外部对指针没有引用时,优先分配在栈上;当函数外部对指针存在引用时,优先分配在堆上;当函数内部分配一个较大对象时,优先分配在堆上。\[1\] 在Go语言中,内存逃逸分析可以通过一些规则来判断。例如,当使用等号=赋值时,如果Data和Field都是引用类型的数据,则会导致Value逃逸。另外,一些特定的数据类型也会导致逃逸,比如\[\]interface{}、map\[string\]interface{}、map\[interface{}\]interface{}、map\[string\]\[\]string、map\[string\]*int、\[\]*int、func(*int)、func(\[\]string)、chan \[\]string等。具体的规则可以参考引用\[2\]中的示例。\[2\] 此外,栈空间不足也可能导致内存逃逸。当在函数中创建一个较大的切片或数组,并且栈空间不足以容纳它们时,这些切片或数组会逃逸到堆上。例如,在一个函数中创建一个长度为10000的切片,如果栈空间不足,这个切片就会逃逸到堆上。\[3\] 总结来说,内存逃逸是指当一个对象的指针被多个方法或线程引用时,这个指针会逃逸到堆上。在Go语言中,内存逃逸的位置由编译器决定,可以通过一些规则和栈空间的判断来进行分析。 #### 引用[.reference_title] - *1* [golang内存逃逸分析](https://blog.csdn.net/qq_42170897/article/details/127770234)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [golang内存逃逸](https://blog.csdn.net/wanghao3616/article/details/107284523)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [golang——内存逃逸机制](https://blog.csdn.net/weixin_45627369/article/details/127163797)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值