从堆栈到逃逸分析,这其中到底发生了什么?

从堆栈到逃逸分析,这其中到底发生了什么?

首页休闲益智切片堆栈更新时间:2024-05-09

一个程序占用的内存分为以下几个部分:

OS给一个进程分配的内存空间大致可以分为:代码区、全局数据区、栈(stack)、堆(heap)、环境变量区域以及中间空白的缓冲区六个部分。其中,数据的增长路径除栈(stack)是由高到低之外,其余的均是由低到高。

那么在写go的时候,变量到底分配在栈中还是在堆中,这个就不是程序员来决定的,而是go自行处理。可能你new出来的变量在堆上,也有可能在栈上。

那么我们是否有办法知道我们写的变量位置在哪呢?

一、逃逸分析

go向开发者提供了变量逃逸分析的工具

$ go run -gcflags "-m -l" main.go

package main import "fmt" func main() { a,b := 1,2 ans := add(a,b) fmt.Println(ans) } func add(a, b int) int { return a b }

打印出来如下内容

.\main.go:8:13: ans escapes to heap .\main.go:8:13: main ... argument does not escape 3

这就表明了变量ans逃逸到了堆中。

二、逃逸原理

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

在栈上效率肯定要比在堆上高。一个理想的逃逸分析算法自然是将能分配在栈上的变量尽可能保留在栈上,尽可能少“逃逸”到堆上。

在[cmd/compile/internal/gc/escape.go]文件中,提到了逃逸分析的设计原理。


两个不变性

逃逸分析的输入是go编译器解析了源文件后获取的整个程序的抽象语法树AST:

1、首先,构建一个有向加权图,其中顶点(locations)表示语句和表达式分配的变量,边表示变量之间的赋值(权重表示寻址/取址次数)

2、接下来,遍历该有向加权图,在图中寻找可能违反上述两个不变性的赋值路径。如果一个变量v的地址是存储在堆上或可能超过存活期,那么v就会被标记需要在堆上分配。

3、为了支持函数间分析,算法还记录了从每个函数的参数到堆的数据流及其结果的数据流。这被称为“参数标签”,用来静态调用,以改进函数参数间的逃逸分析。


源码解析后得到的抽象语法树AST的Node切片为xtop:

var xtop []*Node

在MAIN函数中,注册了逃逸分析

func Main(archInit func(*Arch)) { ... // Phase 6: Escape analysis. // Required for moving heap allocations onto stack, // which in turn is required by the Closure implementation, // which stores the addresses of stack variables into the closure. // If the closure does not escape, it needs to be on the stack // or else the stack copier will not update it. // Large values are also moved off stack in escape analysis; // because large values may contain pointers, it must happen early. timings.Start("fe", "escapes") escapes(xtop) }三、如何逃逸

func escapes(all []*Node) { visitBottomUp(all, escapeFuncs) }

这个visitBottomUp其实就是遍历有向加权图

func visitBottomUp(list []*Node, analyze func(list []*Node, recursive bool)) { var v bottomUpVisitor v.analyze = analyze v.nodeID = make(map[*Node]uint32) for _, n := range list { if n.Op == ODCLFUNC && !n.Func.IsHiddenClosure() { v.visit(n) } } }

真正分析逃逸分析的是escapeFuncs,它是对最小批的函数执行逃逸分析

func escapeFuncs(fns []*Node, recursive bool) { // 如果不是函数类型,报错 for _, fn := range fns { if fn.Op != ODCLFUNC { Fatalf("unexpected node: %v", fn) } } var e Escape e.heapLoc.escapes = true // Construct data-flow graph from syntax trees. for _, fn := range fns { e.initFunc(fn) } for _, fn := range fns { e.walkFunc(fn) } e.curfn = nil e.walkAll() e.finish(fns) }

具体逃逸分析逻辑还是比较复杂的,我们只需要知道逃逸分析是根据有向加权图根据两个不变性进行的分析。

四、实战

既然知道了这两个不变性,那我们可以对这两个不变性做一下文章。

1、简单逃逸

func main() { foo() e,f := boo() println("out e: ",&e) println("out f: ",&f) } func foo() { a,b := 11, new(int) println("a: ", &a) println("b: ",&b) } func boo() (*int, *int) { c,d := 11,12 println("c: ",&c) println("d: ",&d) return &c,&d }

变量c和d因为在外部被使用了,所以会逃逸,其他变量只是在函数内部使用,所以不会逃逸。

$ go run -gcflags="-m -l" main.go .\main.go:11:16: new(int) does not escape .\main.go:17:2: moved to heap: c .\main.go:17:4: moved to heap: d

这个与我们分析结果一致。

2、切片逃逸

func main() { noEscapesliceInHeap() noEscapesliceInStack() escapeslice() } func noEscapesliceInHeap() { var s1 []int println("s1: ",&s1) s1 = append(s1, 1,2,3) println("s1: ",&s1) } func noEscapesliceInStack() { var s2 = make([]int,0,4) println("s2: ",&s2) s2 = append(s2, 1,2,3) println("s2: ",&s2) } func escapeslice() *[]int { var s3 = make([]int, 0, 4) println("s3: ",&s3) return &s3 }

我们在slice时分析过,当slice发生扩容时会重新分配内存,这一步是在堆上操作的。

$ go run -gcflags="-m -l" main.go .\main.go:17:15: make([]int, 0, 4) does not escape .\main.go:24:6: moved to heap: s3 .\main.go:24:15: make([]int, 0, 4) escapes to heap3、fmt逃逸

func main() { foo() } func foo() { a,b := 11,12 println("a: ",&a) println("b: ",&b) fmt.Printf("a:%d ",&a) }

$ go run -gcflags="-m -l" main.go .\main.go:10:2: moved to heap: a .\main.go:13:12: ... argument does not escape

此时变量a还没有逃逸

func foo() { a,b := 11,12 println("a: ",&a) println("b: ",&b) fmt.Printf("a:%d ",a) }

$ go run -gcflags="-m -l" main.go .\main.go:13:12: ... argument does not escape .\main.go:13:13: a escapes to heap

此时变量a已经逃逸

4、手动避免逃逸

在源码中,有这么一个函数

func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) // 任何数值与0的异或都是原数 }

实现逻辑使得我们传入的指针值与其返回的指针值一样,只是通过uintptr做了一次转换,而这次转换将指针转换成数值,“切断”了逃逸分析的数据流,导致传入的指针避免逃逸。

那么我们也可以使用这个函数

func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) // 任何数值与0的异或都是原数 } func foo() { a := 11 b := 12 fmt.Printf("a=%d\n",noescape(unsafe.Pointer(&a))) println("addr of a: ",&a) println("addr of b: ",&b) } func main() { foo() }

$ go run -gcflags="-m -l" main.go .\main.go:7:15: p does not escape .\main.go:13:2: moved to heap: a .\main.go:14:2: moved to heap: b

这样就变量a就不会逃逸了

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved