Golang逃逸分析小记

Golang的逃逸分析旨在确定变量应存储在栈上还是堆上,以优化内存管理和提高性能。本文通过实例探讨了何时会发生逃逸,以及如何影响变量的生命周期和内存分配。逃逸分析能够避免不必要的堆分配,提升程序效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简要的来说,go对变量进行逃逸分析的目的是决定变量应该放在栈上还是堆上,当然变量尽可能地要放在栈上(因为goroutine的栈是可以动态扩缩容的,而不是仅限于操作系统设定的,这样当函数返回时变量占用的内存空间就自动回收了),而堆上的内存就需要使用GC机制去进行管理。

对变量进行逃逸分析可以使go语言中在语言层面提供一些语法糖,例如如下写法是很常见的:

func NewString() *string {
	s := ""
	return &s
}

而如果在C++中这样写,则很明显返回了一个栈变量的指针,使用该指针进行访问是未定义行为:

string* NewString() {
	string s;
	return &s;
}

因为go在编译过程中对变量s进行了逃逸分析,并决定将变量放在堆中,以便于函数返回之后该变量仍然能被合法的使用,若使用go build -gcflags=-m ...来编译便可以看到如下说明:

./new_string.go:5:5: moved to heap: s

具体哪些情况下会发生逃逸,可以参考 Go Escape Analysis Flaws

本文主要是看到这篇博客后对noescape函数的解析,我在go string源码中看到这个函数是这样使用的:

type Builder struct {
	addr *Builder // of receiver, to detect copies by value
	buf  []byte
}
// This was copied from the runtime; see issues 23382 and 7921.
//go:nosplit
//go:nocheckptr
func noescape(p unsafe.Pointer) unsafe.Pointer {
	x := uintptr(p)
	return unsafe.Pointer(x ^ 0)
}
func (b *Builder) copyCheck() {
	if b.addr == nil {
		// This hack works around a failing of Go's escape analysis
		// that was causing b to escape and be heap allocated.
		// See issue 23382.
		// TODO: once issue 7921 is fixed, this should be reverted to
		// just "b.addr = b".
		b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
	} else if b.addr != b {
		panic("strings: illegal use of non-zero Builder copied by value")
	}
}

这个场景其实就是将一个指针赋值给了成员变量,通过noescape函数避免将变量b逃逸到堆中而节省内存分配的过程。可以通过几个例子加深对逃逸分析的理解。

例1

package main
type Foo struct {
    s *string
}
func main() {
    var f Foo
    var s string
    f.bar(&s)
}
//go:noinline
func (f *Foo) bar(s *string) {
    f.s = s
}

这个例子其实就是对string代码中的简化版,s被判定为逃逸,因为出现了非输入到输出的引用(即赋值给了一个成员变量),go编译器为了简便起见,不对有类似情况的变量进行更多的逃逸追踪和分析,而是直接判定为分配到堆中。

例2

package main
type Foo struct {
    s *string
}
func main() {
    var f Foo
    var s string
    bar(f, &s)
}
//go:noinline
func bar(f Foo, s *string) {
    f.s = s
}

这个例子同样可以赋值给一个成员变量,但是s却没有逃逸,因为f变量是传值赋值,没有发生逃逸,分配在栈中,因此f.s的生命周期也存在于这个栈中,所以对s的引用是确保在这个函数之内发生的,在bar函数栈存在的时候,s必然存在于main函数栈中,因此s不需要逃逸。加上这个例子,可以归纳为:当发生非输入到输出的栈外引用时,变量逃逸。

例3

package main
type Foo struct {
    s *string
}
func main() {
    var f Foo
    var s string
    s2 := bar1(&s)
    f.bar2(s2)
}
//go:noinline
func bar1(s *string) *string {
    return s
}
//go:noinline
func (f *Foo) bar2(s *string) {
    f.s = s
}

这里例子可以验证上述判断,得到输出:

./m3.go:15:11: leaking param: s to result ~r1 level=0
./m3.go:20:7: f does not escape
./m3.go:20:20: leaking param: s
./m3.go:9:9: moved to heap: s

bar1中参数s流至返回值~r1,返回值又流入bar2,在bar2中被判定为逃逸,因此找到引用的来源s,判定为逃逸。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值