Go —— 逃逸分析

2024-03-17 04:36
文章标签 分析 go 逃逸

本文主要是介绍Go —— 逃逸分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

逃逸分析

  逃逸分析是指由编译器决定内存分配的位置,不需要程序员指定。在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束后可自动将内存回收
  • 如果分配在堆中,则函数执行结束后可交给GC(垃圾回收)处理

  有了逃逸分析,返回函数局部变量变得可能。除此之外,逃逸分析还跟闭包息息相关,了解哪些场景下对象会逃逸至关重要。

1. 逃逸策略

  在函数中申请新的对象时,编译器会根据该对象是否被函数外部引用来决定是否逃逸:

  • 如果函数外部没有引用,则优先放到栈中
  • 如果函数外部存在引用,则必定放在堆中

对于仅在函数内部使用的变量,也有可能放到堆中,比如内存过大超过栈的存储能力。

2. 逃逸场景

1)指针逃逸

  Go可以返回局部变量指针,这是一个典型的变量逃逸案例。示例代码如下:

package maintype Student struct {Name stringAge  int
}func StudentRegister(name string, age int) *Student {s := new(Student)s.Name = names.Age = agereturn s
}
func main() {StudentRegister("Jim", 18)
}

   函数 StudentRegister() 内部的s为局部变量,其值通过函数值返回,s本身为一个指针,其指向的内存地址不会是栈,而是堆,这就是典型的逃逸案例。
   通过编译参数 -gcflags=-m 可以查看编译过程中的逃逸分析过程:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:8:6: can inline StudentRegister
./main.go:14:6: can inline main
./main.go:15:17: inlining call to StudentRegister
./main.go:8:22: leaking param: name
./main.go:9:10: new(Student) escapes to heap
./main.go:15:17: new(Student) does not escape
PS D:\Go\workspace\src\lekou> 

  在 StudentRegister() 函数中,代码第9行显示 “escapes to heap” ,表示该行内存分配发生了逃逸现象。

2)栈空间不足逃逸

  当栈空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中。示例代码:

package mainfunc Slice() {s1 := make([]int, 1000, 1000)s2 := make([]int, 10000, 10000)for index, _ := range s1 {s1[index] = index}for index, _ := range s2 {s2[index] = index}
}
func main() {Slice()
}

  s1的长度为1000,s2的长度为10000。查看编译提示,如下:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:14:6: can inline main
./main.go:15:7: inlining call to Slice
./main.go:4:12: make([]int, 1000, 1000) does not escape		// s1
./main.go:5:12: make([]int, 10000, 10000) escapes to heap	// s2
./main.go:15:7: make([]int, 1000, 1000) does not escape
./main.go:15:7: make([]int, 10000, 10000) escapes to heap

  可以发现s1没有发生逃逸,s2发生逃逸。

3)动态类型逃逸

  很多函数的参数为 interface 类型,比如 fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸,如以下代码所示。

package mainimport "fmt"func main() {s := "Hello World!"fmt.Println(s)
}

  上述代码中的 s 变量只是一个 string类型变量,调用 fmt.Println()时会产生逃逸:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:7:13: inlining call to fmt.Println
./main.go:7:13: ... argument does not escape
./main.go:7:14: s escapes to heap

4)闭包引用对象逃逸

  某著名的开源框架实现了某个返回Fibonacci数列的函数:

func Fibonacci() func() int {a, b := 0, 1return func() int {a, b = b, a+breturn a}
}

  该函数返回一个闭包,闭包引用了函数的局部变量 a 和 b,使用时通过该函数获取闭包,然后每次执行闭包都会依次输出 Fibonacci 数列。完整的示例程序如下:

package mainimport "fmt"func Fibonacci() func() int {a, b := 0, 1return func() int {a, b = b, a+breturn a}
}func main() {f := Fibonacci()for i := 0; i < 10; i++ {fmt.Printf("Fibonacci:%d\n", f())}
}

  上述代码通过 Fibonacci()获取一个闭包,每次执行闭包就会打印一个 Fibonacci 数值。输出如下:

C:\Users\666\AppData\Local\Temp\GoLand\___293go_build_lekou_.exe
Fibonacci:1
Fibonacci:1
Fibonacci:2
Fibonacci:3
Fibonacci:5
Fibonacci:8
Fibonacci:13
Fibonacci:21
Fibonacci:34
Fibonacci:55Process finished with the exit code 0

  Fibonacci() 函数中原本属于局部变量的 a 和 b 由于闭包的引用,不得不将二者放到堆中,以致产生逃逸:

PS D:\Go\workspace\src\lekou> go build -gcflags=-m
# lekou
./main.go:3:6: can inline Slice
./main.go:10:6: can inline main
./main.go:11:7: inlining call to Slice
./main.go:4:11: make([]int, 1000, 1000) does not escape
./main.go:14:16: inlining call to Fibonacci
./main.go:7:9: can inline main.Fibonacci.func1
./main.go:17:35: inlining call to main.Fibonacci.func1
./main.go:17:13: inlining call to fmt.Printf
./main.go:6:2: moved to heap: a
./main.go:6:5: moved to heap: b
./main.go:7:9: func literal escapes to heap
./main.go:14:16: func literal does not escape
./main.go:17:13: ... argument does not escape
./main.go:17:35: ~r0 escapes to heap

3. 小结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配内存不需要GC处理
  • 对上分配的内存使用完毕会交给GC处理
  • 逃逸分析的目的是决定分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

4. 问题

  思考一下这个问题:函数传递指针真的比传值的效率高吗?

  我们知道传递指针可以减少底层值的复制,可以提高效率,但是如果复制的数据量小,由于指针传递会产生逃逸,则可能会使用堆,也可能增加GC的负担,所以传递指针不一定是高效的。

这篇关于Go —— 逃逸分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/817807

相关文章

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

python中Hash使用场景分析

《python中Hash使用场景分析》Python的hash()函数用于获取对象哈希值,常用于字典和集合,不可变类型可哈希,可变类型不可,常见算法包括除法、乘法、平方取中和随机数哈希,各有优缺点,需根... 目录python中的 Hash除法哈希算法乘法哈希算法平方取中法随机数哈希算法小结在Python中,

Java Stream的distinct去重原理分析

《JavaStream的distinct去重原理分析》Javastream中的distinct方法用于去除流中的重复元素,它返回一个包含过滤后唯一元素的新流,该方法会根据元素的hashcode和eq... 目录一、distinct 的基础用法与核心特性二、distinct 的底层实现原理1. 顺序流中的去重

Go语言代码格式化的技巧分享

《Go语言代码格式化的技巧分享》在Go语言的开发过程中,代码格式化是一个看似细微却至关重要的环节,良好的代码格式化不仅能提升代码的可读性,还能促进团队协作,减少因代码风格差异引发的问题,Go在代码格式... 目录一、Go 语言代码格式化的重要性二、Go 语言代码格式化工具:gofmt 与 go fmt(一)