go

go题目

由一道go题目引发对内存分配和内存逃逸思考

Posted by Liangjf on October 15, 2020

由一道go题目引发对内存分配和内存逃逸思考

题目

func question1() {
	staff := map[string]int{
		"a": 33,
		"b": 29,
		"c": 8,
	}

	var n []*int
	for _, s := range staff {
		fmt.Printf("%d\t%p\n", s, &s)
		n = append(n, &s)
	}
	fmt.Println("=================")
	for _, item := range n {
		fmt.Printf("%d\t%p\n", *item, item)
	}
}

结果

33      0xc00000a0c0
29      0xc00000a0c0
8       0xc00000a0c0
=================
8       0xc00000a0c0
8       0xc00000a0c0
8       0xc00000a0c0

结果分析

从这道题看出2点。一个是内存分配问题。一个是内存逃逸问题。输出以上结果的原因是:内存逃逸使得变量s由栈的临时变量内存逃逸到堆上创建,因此for循环每次取s的地址都是一致的,都是堆上的地址。因此最终第二个for循环打印的结果是一样的。至于输出 8 8 8, 29 29 29 还是 33 33 33,由于map的无序性,是不确定的。

内存逃逸分析

以下是内存逃逸分析 go build -gcflags=-m

# test
.\main.go:14:13: inlining call to fmt.Printf
.\main.go:17:13: inlining call to fmt.Println
.\main.go:19:13: inlining call to fmt.Printf
.\main.go:23:6: can inline main
.\main.go:13:9: moved to heap: s
.\main.go:6:25: question1 map[string]int literal does not escape
.\main.go:14:14: s escapes to heap
.\main.go:14:29: &s escapes to heap
.\main.go:14:13: question1 []interface {} literal does not escape
.\main.go:14:13: io.Writer(os.Stdout) escapes to heap
.\main.go:17:14: "=================" escapes to heap
.\main.go:17:13: question1 []interface {} literal does not escape
.\main.go:17:13: io.Writer(os.Stdout) escapes to heap
.\main.go:19:26: *item escapes to heap
.\main.go:19:26: item escapes to heap
.\main.go:19:13: question1 []interface {} literal does not escape
.\main.go:19:13: io.Writer(os.Stdout) escapes to heap
<autogenerated>:1: (*File).close .this does not escape
<autogenerated>:1: (*File).isdir .this does not escape

在go中,在编译器就会确定内存分配在栈还是堆上,由于第二个for循环用到了切片n的元素(指针),因此 s 会在编译期发生内存逃逸,在堆上创建。

特别关注以下两句:

.\main.go:13:9: moved to heap: s
.\main.go:14:14: s escapes to heap
.\main.go:14:29: &s escapes to heap

可见,确实是发生了内存逃逸,在堆上创建变量了。

常见内存逃逸情况:

  • 发送指针或带有指针的值到 channel
  • 在切片上存储指针或带指针的值
  • slice 运行时发生扩容
  • 在 interface 类型上调用方法