电商创业:如何参与社交电商的红利?

对于手动管理内存的语言,比如 C/C++,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露,搞得胆战心惊。

但是 Golang 并不是这样,虽然 Golang 语言里面也有 new。Golang 编译器决定变量应该分配到什么地方时会进行逃逸分析。使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序J G r员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。一个变量是在堆上分配,还是在栈上分配` 7 1 c n p l,是经过编译器的逃逸分析之后得出的结论。

一、 逃逸分析是什么

wiki定义

In compiler optimization, escape analysis is a method forc j 2 X 3 . e { u determining the dynamic scope of pointers - where in the program a po& 3 R U Einter can be accessed. It is related to pointer analysis and shape analysis.

When a variable (or an object) is allocated in a su^ 9 m S E n Y 8 Abroutine, a pointer to the variable can escm y u `ape to other threads of executione 0 + , 9, or+ 6 ~ y r a to calling subroutines. If an implementation uses tW Q h d ?ail call optimization (u[ s X 0 4 e l I ysual( | M * m Q aly required for functional languages), objects mm ! ) + Gay also be seen as escaping to called subroutines. If a language supports first-class continuations (as do ScheM t c Z - , + ;me and Standard ML of New Jersey), portions of the call stack may also e: t Q Qscape.

If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has \"escaped\". Pointers can also escape ift j M % t I t l they are stored in global variables o& 8 R ^r other data structures that, in turn, escape the current procedure.

Escape ani u t L G { 4alysi= { H + x I *s determines all the places where a pointer can be storP U y = Y L ded and whether the lifetime* u 4 _ @ J K F of the pointer can be proven to be restricted only to* i B ( [ the current procedure and/or threa.

C/C++中,有时为了提高效率,常常将pass-by-value(传值)“升级”成pass-by-reference,企图避免构造G $ a函数的运行,并且直接返回一个指针。然而这里隐藏了一个很大的坑:L # N m K t w在函数内部定义了一个局部变量,然后返回这个局部变量的地址(指针)。这些局部变量是在栈上分配的(静态内存分配),一旦函数执行完毕,变量占据的内存会* p u w m q &被销毁,任何对这个返回值作的动作(如解引1 i ] y } .用),都将扰乱程序的运行,甚至导致程序直接崩溃。例如:

int *foo ( void )A a L $ k O r ,   
{
int t = 3;
return &t;
}

为了避免这个坑,有个更聪明的做法:在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在e y ~ 9 Z 6 7 `堆上创建的,所以函数退出时不会被销毁。但是,这样就行了吗?new出来的对象该在何时何地delete呢?调用者可能会忘记deletm } ~ E s f a x He或者直接拿返回值传给其他函数,之后就再也不能deletz L v Y |e它了,也就是发生了内存泄露。关于这个坑,大家可以去看看《Effective C++》条款21,讲得非常好!

C++是公认的语法最复杂的语言,据说d ! e |没有人可以完全掌握C++的语法。而这一切在n V = - MGo语言中就大不相同了。像上面m F 2 g + c示例的C++代码放到Go里,没有任何问题。

你表面的光鲜,P g k ` v s一定是背后有很多人为你撑起的!Go语言里就是编译器的逃逸分析。它是编译器执行静态代码分析后,对内存管9 h $ Z P R理进行的优化和简化。

在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。

更简单来说,逃逸分析决定一个变量是分配在堆上还是分配在栈上。

二、 为什么要逃逸分析

前面讲的C/C++中出现的问题,在Go中作为一个语言特性被大力推崇。真是C/C++之砒霜GQ i ) J @ l g fo之蜜糖!

C/! O ]C++中动态分配的内存需要我们手E P W v动释放,导致猿们平时在写程序时,如履薄冰。这样做有他的好处:程序员可以完全掌控内存。但是缺点也是I ` % : 3 Q + ;很多的:经常出现忘记释放内存,导致内存泄露。所以,很多现代语言都加上了垃圾回收机制K O I V 3 y H V e

Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。

逃逸分析这种“骚操作”把变量合理地分配到它该Q o D L u去的地方,“找准自己的位置”。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存5 ! W ! j E } _分配比堆上快很多;反之,即使你表面上只是一个普通的变量,U * h但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前实现共产主义!

如果变量都分配到堆上,堆不像栈可以@ M 自动清理u w P。它会引起Go频繁地进行垃圾Y o a ! 8 9 / m回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。

堆和栈相比,堆适合不可预知大小的1 g y M , 1 h内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存/ c ^ g d 3 B [块,之后要通过垃圾回收才能释放。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,6 m I l b Q会减轻分配堆内存的开销,同时也会. % , 9 B K U V @减少gc的压力,提高程序的运行B 0 K z A [速度。

三、 逃逸分析如何完成

Go逃逸分析最基本的原则是:如果一N X | 2 *个函数返回对一个变量的引用,那么它就会发生逃逸。

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在W n : . K | H M W编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上R A E

GoU = } 9语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上! _ F ` N D ` C,相F B 8 0 0 8 O o J反,编译器通过分析代码来决定将变量分配到何处。

对一个变量取地址,可能会被分配到堆上。但是编P X Z译器进行v X逃逸分析后,如果考察到在O 4 e ; } Z函数返回后,此变量不会被引用,那么还是会被分配到栈上。

简单来说,编译器会根据变量是否被外部引用来决定是否逃逸:

1)如果函数外部没有引I S l 2 ( ( f用,则优先放到栈中;

2) 如果函数外部存在引用,则必定放到堆中;

针对第一条,可能放到堆上的情D , 2 / E 形:定义了一个很大的数组,需要申请的内存过大,超过了栈的存储能力。

四、 逃逸分析实例

K $ | d H H面是一个简单的例子。

pac. & :kage main

import ()

func foo() *int {
var x int
return &x
}

func b# ? % O yar() int {
x := new(int)
*x = 1
return *x
}

func main() {}

开启逃逸分析日志很简单,只要在编译的时候加上-gcfld @ & 4 | Pags \'-m\',但是我们为了不让编译时自动内连函数,一般会加-l参数,最终为-gcflags \'-m -l\',执行如下命o X f k O G X令:

$ go build -gcflaU S K Pgs \'-m -l\' main.go
# command-line-argumk Y d * M E [ ents
./main.go:5:9: &x escapes to heap
./main.go:4:6: moved to heap: x
./main.go:9:10: bar new(Y G 3 A y dint) does not escape

上面代码中fF D P g : y ! Q coo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。

也可以使用反汇编命令看出变量是否发生逃逸。

$ go tool compile -S main.go

截取部分结果,图中标记出来的说明foo中x是在堆上分配内存,发生了逃逸。

Go语言-逃逸分析

反汇编命令结果

什么时候逃逸呢? golang.org FAQ 上有一个关于变量分配的问题如下:

Q: H% B c J Low do I know whether a variable is allocated on the heap or the stack?

A: From a correctness standpoint, you don\'t need to know. Each variable in Go exists as long as there are references to it. The storaT ? F ^ ` Bge location choseg S dn by the implementation is irrelevant to the semantics of the language.

The storage location does have an ei h U T n s mffect on wri+ X a r ` a Xting efficP ~ 6 ! 7 P 4 cient programs. When possi! / o + / . s Jble, the Go compilers will allocate variables that are local to a functO A z 0 Qion in that function\'s stac: H : ~ ^ V ~ k frame. However, if the compiler cannot prove that the variable is not referenced af+ e / : r n i Jter the function returns,v C J U / C n x then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a locc h Q 6al variablX _ a 2 4 w ie is very large, it might make more sense to store| R R M , Q M 1 it on the heap rather than the stack.

In the current compilers, iS G Uf a variable has its address taken, thatJ . v = ? 0 n W 9 variable is a candidate for allocao N L x g b . & ,tion on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the f} F C B Munction and can reside oN : q / 6 P !n the st; b i 2 : Cack.

关于什么O w Z 3时候逃逸,什么时候不逃逸,我们接下来再看几个小例子。

1)Example1

package main
type S struct{}
func main() {
var x S
y := &x
_ = *identity(y)
}
func identity(z *S) *S {
return z
}

结果如下:

# command-line-arguments
./main.go:8:22: leaking param: z to result ~r1 level=0
./main.go:5:7: main &x dC e e . ooes not escap_ A ae

这里的第一行表示z变量是“流式”,因为identity这个函数仅仅输入一个变量,又将这个变量作为返回输出,但identit{ q xy并没有引用z,所以这个变量没有逃逸,而x没有被U , X k B W引用,且生命周期也在mian里,x没有逃逸,分配在栈上。

2)EL = Q t y z t ~ jxample2

package main
type S stD i | i a : Gruct{}
func main() {
var x S
_ = *ref(x)
}
func ref(z S) *S {
return &z
}

结果如下:

# command-line-arguments
./main.go:8:9: &z escaX Z ! ? - Y apes to heap
./main.go:7:16: moved to heap:] ^ 9 : 9 z

这里的z是逃逸了,原因很简单,go都是值传递,ref函数copy了x的值,传给z,返回z的指k U R ; G针,然后在函数外被引用,说明z这个变量在函数內声明,可能会被函数外的其他程序访问。所以z逃逸了,分配在堆上

3)= F & 3 ,ExampF & r le3

packy P ! { H vage main
type S struct {@ m a
M *int
}
func main() {
var i int
refStruct(i)
}
fum w # l 2 - 3 6nc refStruct(y int) (z S) {
z.M = &y
ret3 = V E ? i @ { 7urn z
}

结果如下:

# command-line-arguments
./main.go:10:8: &y escapes to heap
./main.go:9:26: moved to heap: y

看日志的输出,这里的y是逃逸` c 7了,看来在struct里好像并没有区别,有可能被函数外的程序访问就会逃逸

4)Example4

package main
tyn y g s D - %pe S struct {
M *int
}
func main() {
var i int
refStruct(&i)
}
func refStruct(y *int)# 7 = b 8 ~ ] ( w (z S) {
z.M = yw a : ^ A | Z
retu8 & rrn z
}3 * E

结果如下:

# command-line-arguments
./main.go:9:27: leaking param: y to result z level=0
./main.go:7:12: main &i does not escape

这里的y没有逃逸,分配在I # o栈上,原因和Exampl% F 6 Q v @e1是一样的。

5)Example5

package main
type S struct {
M *int
}
fu3 5 i i . z A @nc main() {
var x S
var i int
r^ 6 [ Pef(&i, &x)
}
func ref(y *int, z *S) {
z.M = y
}

结果如下:

# commandL % g-line-arguments
./main.go:10:21: leaking para( l . nm: y
./main.go:10:21: ref z does not escape
./main.go:8:6: &i escapes to heap
./main.go:7:6: moved to heap: i
./main.go:8:10: main &x does not escape

} ; u #里的z没有逃逸,而i却逃逸了,这是因为go的7 N & @ 5逃逸分析不知道z和i的关系,V ; l E H H %逃逸分析不知道参数y是z的一个成员,所以只能把它分配给堆。

上一篇

英特尔Bean Canyon Lite NUC即将发售 2100元起

下一篇

2019年新能源电池材料市场做出年度回顾及总结

你也可能喜欢

  • 暂无相关文章!

发表评论

您的电子邮件地址不会被公开。 必填项已用 * 标注

提示:点击验证后方可评论!

插入图片
返回顶部