golang内存逃逸分析

Written by

in

文章目录
  • go语言编译器会自动决定把一个变量放在堆上还是放在栈上,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围(悬空指针),就可以在栈上,否则则必须分配在堆上。 这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。 我们看如下代码: package main func main() { // 打印返回的指针地址 println(fool()) // 0x1400008e000 } //go:noinline 内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化 func fool() *int { var ( a = 1 b = 2 c = 3 d = 4 e = 5 ) println(&a, &b, &c, &d, &e) // 0x14000066f38 0x14000066f30 0x1400008e000 0x14000066f28 0x14000066f20 return &c } 我们能看到**c**是返回给main 的局部变量,其中它的地址值是 0x1400008e000 很明显与其他的 a b d e 不是连续的。 我们用go tool compile测试一下 ~/workspace/test  go tool compile -m main.go main.go:3:6: can inline main main.go:14:3: moved to heap: c 果然,在编译的时候,c 被编译器判定为逃逸变量,将c 放在堆中开辟 内联: go编译器会对一些小函数进行内联优化,以提升性能。内联优化意味着函数的代码会在调用处直接展开,而不是常规的函数调用。这就导致一些逃逸分析的行为发生变化,类似上面那个代码的内存地址就会是连续的。 什么时候编译器会进行内联优化? 函数体较小:Go编译器更容易将体积较小的函数进行内联 无复杂控制结构:如果函数内没有复杂的循环,条件分支等……内联的可能性更高 函数参数和返回值简单:函数参数和返回值不过于复杂也有助于函数的内联
  • new 出来的变量,内存一定是分配在堆上吗? 还是原来的代码,我们通过new 分开来看看: package main func main() { // 打印返回的指针地址 println(fool()) // 0x1400001a0a0 } //go:noinline 内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化 func fool() *int { var ( a = new(int) b = new(int) c = new(int) d = new(int) e = new(int) ) println(a, b, c, d, e) // 0x14000098f38 0x14000098f30 0x1400001a0a0 0x14000098f28 0x14000098f20 return c } 很明显,c 的地址 0x1400001a0a0 依然和其他的不是连续的内存空间,依然具备逃逸行为。所以这里不是分配在堆上的。 结论: new 并不强制堆分配: 使用 new (T)分配的变量不一定分配在堆上,依然依赖于 Go 编译器的逃逸分析结果。 是否逃逸决定了内存分配的位置:如果变量需要在函数作用域外使用(逃逸),则分配在堆上;如果可以在局部栈中管理,则分配在栈上。
  • 一般我们给一个引用类对象中的引用类成员进行赋值,就可能会出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但是如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,就有可能会出现逃逸现象了。 Go 语言的引用类型有:func(函数类型)、 interface(接口类型)、slice(切片类型)、 map(字典类型)、 channel(管道类型)、 *(指针类型) 等.
  • golang 中的变量内存分配在堆上还是在栈上,是由编译器做逃逸分析之后决定的。 到此这篇关于golang内存逃逸分析的文章就介绍到这了,更多相关golang 内存逃逸内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 您可能感兴趣的文章: golang内存逃逸的学习笔记 golang防止内存逃逸的方法小结 一文搞懂Golang中的内存逃逸 详解Golang的GC和内存逃逸 Golang内存管理之内存逃逸分析 Golang学习之内存逃逸分析 浅析Golang中的内存逃逸 一文搞懂Golang中的内存逃逸 浅谈Golang内存逃逸
  • 目录
    • 一、编译器的逃逸分析
    • 二、new的变量内存分配在栈还是堆上?
    • 三、逃逸规则
      • 案例1
      • 案例2
      • 案例3
      • 案例4
      • 案例5
      • 案例6
    • 四、结论

      go语言编译器会自动决定把一个变量放在堆上还是放在栈上,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围(悬空指针),就可以在栈上,否则则必须分配在堆上。

      这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。

      我们看如下代码:

      package main
      
      func main() {
      	// 打印返回的指针地址
      	println(fool())
      	// 0x1400008e000
      }
      
      //go:noinline    内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化
      func fool() *int {
      	var (
      		a = 1
      		b = 2
      		c = 3
      		d = 4
      		e = 5
      	)
      	println(&a, &b, &c, &d, &e)
      	// 0x14000066f38 0x14000066f30 0x1400008e000 0x14000066f28 0x14000066f20
      	return &c
      }
      

      我们能看到**c**是返回给main 的局部变量,其中它的地址值是 0x1400008e000 很明显与其他的 a b d e 不是连续的。

      我们用go tool compile测试一下

       ~/workspace/test  go tool compile -m main.go
      main.go:3:6: can inline main
      main.go:14:3: moved to heap: c

      果然,在编译的时候,c 被编译器判定为逃逸变量,将c 放在堆中开辟

      内联: go编译器会对一些小函数进行内联优化,以提升性能。内联优化意味着函数的代码会在调用处直接展开,而不是常规的函数调用。这就导致一些逃逸分析的行为发生变化,类似上面那个代码的内存地址就会是连续的。

      什么时候编译器会进行内联优化?

      • 函数体较小:Go编译器更容易将体积较小的函数进行内联
      • 无复杂控制结构:如果函数内没有复杂的循环,条件分支等……内联的可能性更高
      • 函数参数和返回值简单:函数参数和返回值不过于复杂也有助于函数的内联

      new 出来的变量,内存一定是分配在堆上吗?

      还是原来的代码,我们通过new 分开来看看:

      package main
      
      func main() {
      	// 打印返回的指针地址
      	println(fool())
      	// 0x1400001a0a0
      }
      
      //go:noinline    内置的编译指令,可以强制让 Go 编译器不对指定的函数进行内联优化
      func fool() *int {
      	var (
      		a = new(int)
      		b = new(int)
      		c = new(int)
      		d = new(int)
      		e = new(int)
      	)
      	println(a, b, c, d, e)
      	// 0x14000098f38 0x14000098f30 0x1400001a0a0 0x14000098f28 0x14000098f20
      	return c
      }
      
      

      很明显,c 的地址 0x1400001a0a0 依然和其他的不是连续的内存空间,依然具备逃逸行为。所以这里不是分配在堆上的。

      结论:

      • new 并不强制堆分配: 使用 new (T)分配的变量不一定分配在堆上,依然依赖于 Go 编译器的逃逸分析结果。
      • 是否逃逸决定了内存分配的位置:如果变量需要在函数作用域外使用(逃逸),则分配在堆上;如果可以在局部栈中管理,则分配在栈上。

      一般我们给一个引用类对象中的引用类成员进行赋值,就可能会出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但是如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,就有可能会出现逃逸现象了。

      Go 语言的引用类型有:func(函数类型)、 interface(接口类型)slice(切片类型)、 map(字典类型)、 channel(管道类型)、 *(指针类型) 等.

      如果一个函数作为值传递给另一个函数,或者被作为闭包使用,生命周期超出其原始作用域,则它会逃逸。

      package main
      
      func main() {
      	foo()()
      }
      
      //go:noinline
      func foo() func() {
      	return func() {
      		println("call")
      	}
      }
      
      

      通过编译看看逃逸分析:

      ~/workspace/test  go tool compile -m main.go
      main.go:9:9: can inline foo.func1
      main.go:9:9: func literal escapes to heap

      能看到 发生了逃逸现象

      对一个[]interface{} 类型尝试进行赋值,必定出现逃逸

      package main
      
      //go:noinline
      func main() {
      	var a = []interface{}{"100", "1000"}
      	a[0] = 10
      }
      
      

      逃逸分析:

       ~/workspace/test  go tool compile -m main.go
      main.go:5:23: []interface {}{...} does not escape
      main.go:5:24: "100" does not escape
      main.go:5:31: "1000" does not escape
      main.go:6:2: 10 escapes to heap
      

      a[0]=10 发生了逃逸现象

      map[string]interface{}类型尝试通过赋值,必定出现逃逸

      package main
      
      //go:noinline
      func main() {
      	var a = make(map[string]interface{})
      	a["hello"] = "world"
      	a["1"] = "1"
      }
      

      逃逸分析:

       ~/workspace/test  go tool compile -m main.go
      main.go:5:14: make(map[string]interface {}) does not escape
      main.go:6:2: "world" escapes to heap
      main.go:7:2: "1" escapes to heap
      

      a["hello"] = "world" a["1"] = "1" 分别都发生了逃逸

      map[interface{}]interface{} 类型尝试通过赋值,会导致key 和 value 的赋值出现逃逸

      package main
      
      //go:noinline
      func main() {
      	var a = make(map[interface{}]interface{})
      	a["hello"] = "world"
      }
      
      

      看看编译结果:

       ~/workspace/test  go tool compile -m main.go
      main.go:5:14: make(map[interface {}]interface {}) does not escape
      main.go:6:2: "hello" escapes to heap
      main.go:6:2: "world" escapes to heap

      我们能看到,key 和 value 均发生了逃逸

      map[string][]string数据类型,赋值 []string 会发生逃逸

      package main
      
      //go:noinline
      func main() {
      	var a = make(map[string][]string)
      	a["hello"] = []string{"word1"}
      }
      

      通过逃逸分析发现:

       ~/workspace/test  go tool compile -m main.go
      main.go:5:14: make(map[string][]string) does not escape
      main.go:6:23: []string{...} escapes to heap
      

      []string{…} 切片发生了逃逸

      []*int 数据类型,赋值的右侧会发生逃逸

      package main
      
      //go:noinline
      func main() {
      	var a []*int
      	var b = 3
      	a = append(a, &b)
      }
      
      

      逃逸分析:

       ~/workspace/test  go tool compile -m main.go
      main.go:6:6: moved to heap: b
      

      其中 将 b 追加到 a 切片中, 最终 b 发生了逃逸

      golang 中的变量内存分配在堆上还是在栈上,是由编译器做逃逸分析之后决定的。

      到此这篇关于golang内存逃逸分析的文章就介绍到这了,更多相关golang 内存逃逸内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

      您可能感兴趣的文章:

      • golang内存逃逸的学习笔记
      • golang防止内存逃逸的方法小结
      • 一文搞懂Golang中的内存逃逸
      • 详解Golang的GC和内存逃逸
      • Golang内存管理之内存逃逸分析
      • Golang学习之内存逃逸分析
      • 浅析Golang中的内存逃逸
      • 一文搞懂Golang中的内存逃逸
      • 浅谈Golang内存逃逸

      站内搜索