我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Go语言中的逃逸分析究竟是什么?

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Go语言中的逃逸分析究竟是什么?

1、逃逸分析介绍

学计算机的同学都知道,在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了“逃逸”。

Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。

写过C/C++的小伙伴应该知道,使用比较经典的mallocnew函数可以在堆上分配一块内存,这块内存的使用和回收(销毁)的任务在程序员中,处理不当,很可能会发生内存泄露。

2、Go中内存分配在哪里?

但是在Go语言中,基本不用担心内存泄露的问题,因为内存回收Go语言中已经帮我们处理了(GC回收机制)。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。

Go语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。

Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。

对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。

编译器会根据变量是否被外部引用来决定是否逃逸:

  • 如果在函数外面没有引用到,则优先放到栈区中;
  • 如果在函数外面存在引用的可能,则就会放到堆区中;

当我们写C/C++代码时,为了提高效率,会经常将pass-by-value(传值)提升成pass-by-reference,企图避免构造函数的运行,并且直接返回一个指针。

你一定还记得,这里隐藏了一个很大的坑:在函数内部定义了一个局部变量,然后返回这个局部变量的地址(指针)。这些局部变量是在栈上分配的(静态内存分配),一旦函数执行完毕,变量占据的内存会被销毁,任何对这个返回值作的动作(如解引用),都将扰乱程序的运行,甚至导致程序直接崩溃。比如下面的这段代码:


int *foo ( void )   
{   
    int t = 3;
    return &t;
}

有些同学可能知道上面这个坑,用了个更聪明的做法:在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在堆上创建的,所以函数退出时不会被销毁。

但是,这样就行了吗?new出来的对象该在何时何地delete呢?调用者可能会忘记delete或者直接拿返回值传给其他函数,之后就再也不能delete它了,也就是发生了内存泄露。关于这个坑,大家可以去看看《Effective C++》条款21,讲得非常好!

3、Go与C++内存分配的区别

上面讲的C/C++中会遇到的问题,在Go中作为一个语言特性被大力推崇,可以解决以上的难点!

C/C++中的动态分配的内存需要我们手动来释放,这样会带来一个问题:有些内存处理不当或回收不及时,导致内存泄露。

但是这样的好处是:开发人员可以自己管理内存。

Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。

4、逃逸分析骚操作

逃逸分析这种“骚操作”把变量合理地分配到它该去的地方。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。

如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。

堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。

5、逃逸分析引申示例说明

引申1:如何查看某个变量是否发生了逃逸?两种方法:使用go命令,查看逃逸分析结果;反汇编源码;

比如用这个例子:


package main
import "fmt"
func foo() *int {
    t := 3
    return &t;
}
func main() {
    x := foo()
    fmt.Println(*x)
}

使用go命令:


go build -gcflags '-m -l' main.go


加-l是为了不让foo函数被内联。得到如下输出:


# 命令行变量
class="lazy" data-src/main.go:7:9: &t escapes to heap
class="lazy" data-src/main.go:6:7: moved to heap: t
class="lazy" data-src/main.go:12:14: *x escapes to heap
class="lazy" data-src/main.go:12:13: main ... argument does not escape

foo函数里的变量t逃逸了,和我们预想的一致。让我们不解的是为什么main函数里的x也逃逸了?这是因为有些函数参数为interface类型,比如fmt.Println(a …interface{}) ,编译期间很难确定其参数的具体类型,也会发生逃逸。

反汇编代码比较难理解,这里就不讲了。

引申2:下面代码中的变量发生逃逸了吗?

先来看示例1:


package main
type S struct {}
func main() {
  var x S
  _ = identity(x)
}
func identity(x S) S {
  return x
}

分析:Go语言函数传递都是通过值的,调用函数的时候,直接在栈上copy出一份参数,不存在逃逸。

 再来看示例二:


package main
type S struct {}
func main() {
  var x S
  y := &x
  _ = *identity(y)
}
func identity(z *S) *S {
  return z
}

分析:identity函数的输入直接当成返回值了,因为没有对z作引用,所以z没有逃逸。对x的引用也没有逃出main函数的作用域,因此x也没有发生逃逸。

 继续看示例三:


package main
type S struct {}
func main() {
  var x S
  _ = *ref(x)
}
func ref(z S) *S {
  return &z
}

分析:z是对x的拷贝,ref函数中对z取了引用,所以z不能放在栈上,否则在ref函数之外,通过引用如何找到z,所以z必须要逃逸到堆上。仅管在main函数中,直接丢弃了ref的结果,但是Go的编译器还没有那么智能,分析不出来这种情况。而对x从来就没有取引用,所以x不会发生逃逸。

还有示例四:如果对一个结构体成员赋引用如何?


package main
type S struct {
  M *int
}
func main() {
  var i int
  refStruct(i)
}
func refStruct(y int) (z S) {
  z.M = &y
  return z
}

分析:refStruct函数对y取了引用,所以y发生了逃逸。

 最后看示例五:


package main
type S struct {
  M *int
}
func main() {
  var i int
  refStruct(&i)
}
func refStruct(y *int) (z S) {
  z.M = y
  return z
}

分析:main函数里对i取了引用,并且把它传给了refStruct函数,i的引用一直在main函数的作用域用,因此i没有发生逃逸。和上一个例子相比,有一点小差别,但是导致的程序效果是不同的:例子4中,i先在main的栈帧中分配,之后又在refStruct栈帧中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通过引用传递。

到此这篇关于Go语言中的逃逸分析究竟是什么?的文章就介绍到这了,更多相关Go语言中的逃逸内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Go语言中的逃逸分析究竟是什么?

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Go语言中内存管理逃逸的方法是什么

本篇内容介绍了“Go语言中内存管理逃逸的方法是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1. 前言所谓的逃逸分析(Escape an
2023-07-05

Go语言中内存管理逃逸分析详解

所谓的逃逸分析(Escape analysis)是指由编译器决定内存分配的位置吗不需要程序员指定。本文就来和大家简单分析一下Go语言中内存管理逃逸吧
2023-03-15

简述Java编程语言中的逃逸分析

大家一般认为new出来的对象都是被分配在堆上,但这并不是完全正确,通过对Java对象分配过程分析,我们发现对象除了可以被分配在堆上,还可以在栈或TLAB中分配空间。而栈上分配对象的技术基础是逃逸分析和标量替换,本文主要介绍下
2023-05-31

Go语言泛型分析:其泛型的实质究竟何在?

Go语言泛型分析:其泛型的实质究竟何在?近年来,Go语言一直被诟病缺乏泛型支持,这成为了许多开发者探讨的焦点。随着Go语言社区的不断发展和对泛型的需求日益增长,Go语言团队在1.18版本中引入了泛型支持。这一举措引发了广泛的讨论和期待,那
Go语言泛型分析:其泛型的实质究竟何在?
2024-03-15

Go语言底层实现原理揭秘:底层语言究竟是什么?

Go语言底层实现原理揭秘:底层语言究竟是什么?在计算机科学领域中,底层语言通常指的是可以直接与硬件交互的编程语言,它可以更加精细地控制计算机的底层资源,包括内存、寄存器等。作为一种高级编程语言,Go语言在应用层提供了强大简洁的特性,但是G
Go语言底层实现原理揭秘:底层语言究竟是什么?
2024-03-07

详解Objective-C中的语法糖@{}究竟是什么

最近在技术群里有一个群友提出了一个问题,就是为什么下面代码打印的结果不一样?NSMutableDictionary *mDic1 = [NSMutableDictionary dictionaryWithDictionary:@{@"a":
2022-05-23

go语言中泛型是的是什么

今天小编给大家分享的是go语言中泛型是的是什么,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获的哦。在go语言中,泛型就是编写模板适应所有类型,只有在具体使用时才定义具体变量类型;通过引入类
2023-07-04

Go语言中的反射是什么

本篇内容介绍了“Go语言中的反射是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是反射反射,嗯...,就是反着的意思呗,就是把东西反
2023-06-15

go语言中sql指的是什么

今天小编给大家分享一下go语言中sql指的是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。SQL是指“结构化查询语言”,
2023-07-04

go语言中str指的是什么

本篇内容主要讲解“go语言中str指的是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“go语言中str指的是什么”吧!在go语言中,str指的是“字符串”,是一个不可改变的字节序列。字符串可
2023-07-04

Go语言中的channel是什么意思

Go语言中的channel是一种用于协程之间进行通信和数据同步的机制。可以被看作是一种特殊的数据类型,类似于队列或管道,用于在不同的协程之间传递数据。Channel提供了两个主要操作:发送(send)和接收(receive)。在channe
Go语言中的channel是什么意思
2023-12-14

go语言中标识符指的是什么

这篇文章主要介绍“go语言中标识符指的是什么”,在日常操作中,相信很多人在go语言中标识符指的是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”go语言中标识符指的是什么”的疑惑有所帮助!接下来,请跟着小编
2023-07-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录