golang逃逸分析

栏目: Go · 发布时间: 7年前

内容简介:带GC语言给我们程序的编写带来了极大的便利,但是与此同时屏蔽了很多底层的细节,比如一个对象是在栈上分配还是在堆上分配。对于普通的代码来说虽然不需要关心这么多,但是作为强迫症程序猿,还是希望能让自己写出来的代码性能最优,所以还是需要了解什么是逃逸,以及如何判断是否发生了逃逸。首先需要知道,我们说的堆和栈是啥。这个可不是数据结构里面的”堆”和”栈”,而是操作系统里面的概念。在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式

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

什么是堆和栈?

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

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

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

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

这个问题要从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的当我没说),所以我们需要通过一些方法来确定某个变量到底是分配在了栈上还是堆上。

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

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压力了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入理解计算机系统(英文版·第2版)

深入理解计算机系统(英文版·第2版)

[美] Randal E. Bryant、[美] David R. O'Hallaron / 机械工业出版社 / 2011-1 / 128.00元

本书是一本将计算机软件和硬件理论结合讲述的经典教程,内容覆盖计算机导论、体系结构和处理器设计等多门课程。本书的最大优点是为程序员描述计算机系统的实现细节,通过描述程序是如何映射到系统上,以及程序是如何执行的,使读者更好地理解程序的行为为什么是这样的,以及造成效率低下的原因。 相对于第1版,本版主要是反映了过去十年间硬件技术和编译器的变化,具体更新如下: 1. 对系统的介绍(特别是实际使......一起来看看 《深入理解计算机系统(英文版·第2版)》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具