golang逃逸技术分析

申请到栈内存好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
申请到堆上面的内存才会引起垃圾回收。

func F() {
 a := make([]int020)
 b := make([]int020000)

 l := 20
 c := make([]int0, l)
}

a和b代码一样,就是申请的空间不一样大,但是它们两个的命运是截然相反的。a前面已经介绍过,会申请到栈上面,而b,由于申请的内存较大,编译器会把这种申请内存较大的变量转移到堆上面。即使是临时变量,申请过大也会在堆上面申请。
而c,对我们而言其含义和a是一致的,但是编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。

堆(Heap)和栈(Stack)


参考 此文[1] <内存模型:Heap>, <内存模型:Stack>部分的内容:

Heap:

堆的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。


Stack:

栈是由于函数运行而临时占用的内存区域

执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

一般来说,调用栈有多少层,就有多少帧。

所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 被翻译为。 (栈这个字的原始含义,就有栅栏的意思,所谓 栈道,栈桥,都是指比较简陋的用栅栏做的道路/桥梁)


即 在函数中申请一个新的对象:

如果分配 在栈中,则函数执行结束可自动将内存回收;不会引起垃圾回收,对性能没有影响。

如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。


C/C++中的new都是分配到堆上,Go则不一定(Java亦然)[2]



何为逃逸分析(Escape analysis)


在堆上分配的内存,需要GC去回收,而在栈上分配,函数执行完就销毁,不存在垃圾回收的问题. 所以应尽可能将内存分配在栈上.

但问题是,对于一个函数或变量,并不能知道还有没有其他地方在引用. 所谓的逃逸分析,就是为了确定这个事儿~

Go编译器会跨越函数和包的边界进行全局的逃逸分析。它会检查是否需要在堆上为一个变量分配内存,还是说可以在栈本身的内存里对其进行管理。




何时发生逃逸分析?


Go编译器决定变量应该分配到什么地方时会进行逃逸分析


From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.
However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.


Q:如何得知变量是分配在栈(stack)上还是堆(heap)上?

A: 准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

但知道变量的存储位置确实对程序的效率有帮助。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。 当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数 return 之后,变量不再被引用,则将其分配到栈上。


可以使用go命令的 -gcflags="-m"选项,来观察逃逸分析的结果以及GC工具链的内联决策[3] ([内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体本身。这么做的原因是它可以消除函数调用本身的开销,也使得编译器能更高效地执行其他的优化策略。我们可以显式地在函数定义前面加一行//go:noinline 注释让编译器不对函数进行内联)




实例


对于escape1.go代码如下:

package main

import "fmt"

func main() {
 fmt.Println("Called stackAnalysis", stackAnalysis())
}

//go:noinline
func stackAnalysis() int {
 data := 100
 return data
}

通过 go build -gcflags "-m -l" escape1.go go build -gcflags=-m escape1.go 来查看和分析逃逸分析:

./escape1.go:6:13: inlining call to fmt.Println
./escape1.go:6:14"Called stackAnalysis" escapes to heap
./escape1.go:6:51: stackAnalysis() escapes to heap
./escape1.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

escapes to heap 即代表该行该处 内存分配发生了逃逸现象. 变量需要在函数栈之间共享(这个例子就是在main和fmt.Println之间在栈上共享)

  • 第6行第13个字符处的字符串标量"Called stackAnalysis"逃逸到堆上

  • 第6行51个字符处的函数调用stackAnalysis()逃逸到了堆上


对于escape2.go代码如下:

package main

import "fmt"

func main() {
 fmt.Println("Called heapAnalysis", heapAnalysis())
}

//go:noinline
func heapAnalysis() *int {
 data := 100
 return &data
}

执行go build -gcflags=-m escape2.go:

# command-line-arguments
./escape2.go:6:13: inlining call to fmt.Println
./escape2.go:11:2: moved to heap: data
./escape2.go:6:14"Called heapAnalysis" escapes to heap
./escape2.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

函数heapAnalysis返回 int类型的指针,在main函数中会使用该指针变量. 因为是在heapAnalysis函数外部访问,所以data变量必须被移动到堆上

主函数main会从堆中访问该data变量

(可见指针虽能够减少变量在函数间传递时的数据值拷贝,但不该所有类型数据都返回其指针.如果分配到堆上的共享变量太多会增加了GC的压力)




逃逸类型


1. 指针逃逸:


对于 escape_a.go:

package main

type Student struct {
 Name string
 Age  int
}

func StudentRegister(name string, age int) *Student {
 s := new(Student) //局部变量s逃逸到堆

 s.Name = name
 s.Age = age

 return s
}

func main() {
 StudentRegister("dashen"18)
}

执行 go build -gcflags=-m escape_a.go

# command-line-arguments
./escape_a.go:8:6: can inline StudentRegister
./escape_a.go:17:6: can inline main
./escape_a.go:18:17: inlining call to StudentRegister
./escape_a.go:8:22: leaking param: name
./escape_a.go:9:10new(Student) escapes to heap
./escape_a.go:18:17new(Student) does not escape

**s 虽然为 函数StudentRegister()内的局部变量, 其值通过函数返回值返回. 但s 本身为指针类型. 所以其指向的内存地址不会是栈而是堆. **

这是一种典型的变量逃逸案例


2. 栈空间不足而导致的逃逸(空间开辟过大):


对于 escape_b.go:

package main

func InitSlice() {
 s := make([]int10001000)

 for index := range s {
  s[index] = index
 }
}

func main() {
 InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:11:6: can inline main
./escape_b.go:4:11make([]int10001000) does not escape

此时并没有发生逃逸

将切片的容量增大10倍,即:

package main

func InitSlice() {
 s := make([]int100010000)

 for index := range s {
  s[index] = index
 }
}

func main() {
 InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:11:6: can inline main
./escape_b.go:4:11make([]int100010000) escapes to heap

发生了逃逸


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

ps:

package main

func InitSlice() {
 s := make([]int10001000)

 for index := range s {
  s[index] = index
 }
 println(s)
}

func main() {
 InitSlice()
}

执行go build -gcflags=-m escape_b.go

# command-line-arguments
./escape_b.go:12:6: can inline main
./escape_b.go:4:11make([]int10001000) does not escape

没有逃逸.

而改成

package main

import "fmt"

func InitSlice() {
 s := make([]int10001000)

 for index := range s {
  s[index] = index
 }
 fmt.Println(s)
}

func main() {
 InitSlice()
}

执行go build -gcflags=-m escape_b.go,则

# command-line-arguments
./escape_b.go:11:13: inlining call to fmt.Println
./escape_b.go:14:6: can inline main
./escape_b.go:6:11make([]int10001000) escapes to heap
./escape_b.go:11:13: s escapes to heap
./escape_b.go:11:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

发生了逃逸

这是为何? 参见下文!


3. 动态类型逃逸(不确定长度大小):


当函数参数为**interface{}**类型, 如最常用的fmt.Println(a …interface{}), 编译期间很难确定其参数的具体类型,也会产生逃逸


对于 escape_c1.go:

package main

import "fmt"

func main() {
 s := "s会发生逃逸"
 fmt.Println(s)
}

执行go build -gcflags=-m escape_c1.go

# command-line-arguments
./escape_c1.go:7:13: inlining call to fmt.Println
./escape_c1.go:7:13: s escapes to heap
./escape_c1.go:7:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

对于 escape_c1.go:

package main

func main() {
 InitSlice2()
}

func InitSlice2() {
 a := make([]int020)    // 栈 空间小
 b := make([]int020000// 堆 空间过大 逃逸

 l := 20
 c := make([]int0, l) // 堆 动态分配不定空间 逃逸

 _, _, _ = a, b, c
}

执行go build -gcflags=-m escape_c2.go

# command-line-arguments
./escape_c2.go:7:6: can inline InitSlice2
./escape_c2.go:3:6: can inline main
./escape_c2.go:4:12: inlining call to InitSlice2
./escape_c2.go:4:12make([]int020) does not escape
./escape_c2.go:4:12make([]int020000) escapes to heap
./escape_c2.go:4:12make([]int0, l) escapes to heap
./escape_c2.go:8:11make([]int020) does not escape
./escape_c2.go:9:11make([]int020000) escapes to heap
./escape_c2.go:12:11make([]int0, l) escapes to heap

4. 闭包引用对象逃逸:


对于如下斐波那契数列escape_d.go:

package main

import "fmt"

func Fibonacci() func() int {
 a, b := 01
 return func() int {
  a, b = b, a+b
  return a
 }
}

func main() {
 f := Fibonacci()

 for i := 0; i < 10; i++ {
  fmt.Printf("Fibonacci: %d\n", f())
 }
}

执行go build -gcflags=-m escape_d.go

# command-line-arguments
./escape_d.go:7:9: can inline Fibonacci.func1
./escape_d.go:17:13: inlining call to fmt.Printf
./escape_d.go:6:2: moved to heap: a
./escape_d.go:6:5: moved to heap: b
./escape_d.go:7:9func literal escapes to heap
./escape_d.go:17:34: f() escapes to heap
./escape_d.go:17:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

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




总结


  • 逃逸分析在编译阶段完成

  • 逃逸分析目的是决定内分配地址是栈还是堆

  • 栈上分配内存比在堆中分配内存有更高的效率

  • 栈上分配的内存不需要GC处理

  • 堆上分配的内存使用完毕会交给GC处理


通过逃逸分析,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需gc标记清除,从而减少gc的压力

同时,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配,而没有发生逃逸的则有编译器在栈上分配)

另外,还可以进行同步消除: 如果定义的对象的方法上有同步锁,但在运行时却只有一个线程在访问,此时逃逸分析后的机器码会去掉同步锁运行




全文参考自:

Go内存管理之代码的逃逸分析

Golang内存分配逃逸分析[4]


推荐阅读:

golang如何优化编译、逃逸分析、内联优化

java逃逸技术分析[5]

译文 Go 高性能系列教程之三:编译器优化[6]

参考资料

[1]

此文: http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

[2]

Go则不一定(Java亦然): https://dashen.tech/2017/06/18/golang%E4%B8%ADnew-%E5%92%8Cmake-%E7%9A%84%E5%8C%BA%E5%88%AB/

[3]

内联决策: https://dashen.tech/2021/05/22/Go%E4%B8%AD%E7%9A%84%E5%86%85%E8%81%94%E4%BC%98%E5%8C%96/

[4]

Golang内存分配逃逸分析: https://www.cnblogs.com/shijingxiang/articles/12200355.html

[5]

java逃逸技术分析: https://blog.csdn.net/iechenyb/article/details/80925876

[6]

译文 Go 高性能系列教程之三:编译器优化: https://zhuanlan.zhihu.com/p/377397367

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/59862.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动化管理管理工具----Ansible

目录 ​编辑 一、Ansible概念 1.1特点 二、工作机制&#xff08;日常模块&#xff09; 2.1 核心程序 三、Ansible 环境安装部署 四、ansible 命令行模块 4.1command 模块 4.2shell 模块 4.3cron 模块 4.4user 模块 4.5group 模块 4.6copy模块 4.7file模块 4.8ho…

保护网站安全:学习蓝莲花的安装和使用,复现跨站脚本攻击漏洞及XSS接收平台

这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 环境准备 一、XSS基础 1、反射型XSS 攻击介绍 原理 攻击者通过向目标网站提交包含恶意脚本的请求&#xff0c;然后将该恶意脚本注入到响应页面中&#xff0c;使其他用户在查看…

Linux 通过 Docker 部署 Nacos 2.2.3 服务发现与配置中心

目录 环境准备Nacos 数据库创建Docker 部署 Nacos1. 创建挂载目录2. 下载镜像3. 创建和启动容器4. 访问控制台 导入 Nacos 配置SpringBoot 整合 Nacospom 依赖application.yml 配置 参考官方链接微服务商城源码 环境准备 名称版本IP端口Nacos2.2.3192.168.10.218848、9848MySQ…

Linux学习之Ubuntu 20使用systemd管理OpenResty服务

sudo cat /etc/issue可以看到操作系统的版本是Ubuntu 20.04.4 LTS&#xff0c;sudo lsb_release -r可以看到版本是20.04&#xff0c;sudo uname -r可以看到内核版本是5.5.19&#xff0c;sudo make -v可以看到版本是GNU Make 4.2.1。 需要先参考我的博客《Linux学习之Ubuntu 2…

Spring-SpringBoot-SpringMVC-MyBatis常见面试题

文章目录 Spring篇springbean是安全的的?什么是AOP你们工作中有用过AOP吗spring中的事务是如何实现的spring中事务失效场景Spring的生命周期spring中的循坏依赖springMVC的执行流程springboot的启动原理常用注解MyBatis执行流程Mybatis是否支持延迟加载&#xff1f;Mybatis的一…

学习创建第一个 React 项目

目标 本篇的目标是配置好基础的环境并创建出第一个 React 项目。 由于之前没接触过相关的知识&#xff0c;所以还需要了解其依赖的一些概念。 步骤主要参考First React app using create-react-app | VS code | npx | npm - YouTube 0. 简单了解相关概念 JavaScript 一种语…

Python Qt(七)Listview

源代码&#xff1a; # -*- coding: utf-8 -*-# Form implementation generated from reading ui file qt_listview.ui # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not…

CLICK HOUSE

一、clickhouse简介 MPP架构的列式存储数据库&#xff08;DBMS&#xff1a;Database Management System&#xff09;&#xff0c;能够使用 SQL 查询实时生成分析数据报告。ClickHouse的全称是Click Stream&#xff0c;Data WareHouse。 ClickHouse的全称由两部分组成&#xf…

BigDecimal百科全书

一、BigDecimal简述 Java在java.math包中提供的API类BigDecimal&#xff0c;用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数&#xff0c;但在实际应用中&#xff0c;可能需要对更大或者更小的数进行运算和处理。 一般情况下&#xff0c;…

爬虫逆向实战(二十一)-- 某某点集登录与获取数据

登录 一、数据接口分析 主页地址&#xff1a;某某点集 1、抓包 通过抓包可以发现登录接口是phonePwdLogin 2、判断是否有加密参数 请求参数是否加密&#xff1f; 通过查看“载荷”模块可以发现有pwd和sig两个加密参数 请求头是否加密&#xff1f; 无响应是否加密&#x…

ICCV 2023 | 小鹏汽车纽约石溪:局部上下文感知主动域自适应LADA

摘要 主动域自适应&#xff08;ADA&#xff09;通过查询少量选定的目标域样本的标签&#xff0c;以帮助模型从源域迁移到目标域。查询数据的局部上下文信息非常重要&#xff0c;特别是在域间差异较大的情况下&#xff0c;然而现有的ADA方法尚未充分探索这一点。在本文中&#…

C#2010 .NET4 解析 json 字符串

下载Newtonsoft.Json.dll using System; using System.Collections.Generic; using System.Linq; using System.Text;using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; namespace ConsoleApplication1 {class Program{static void Main(string[] args){strin…

MySQL多表联查如何建立索引?

在 MySQL 数据库中&#xff0c;设计索引主要是为了提高查询的效率&#xff0c;降低数据库的压力。当我们进行多表查询时&#xff0c;正确设计索引非常重要。 具体方法与建议&#xff1a; 为连接列创建索引&#xff1a;在多表查询中&#xff0c;连接列通常是性能瓶颈。为这些列…

SpringBoot+MyBatisPlus+MySql+vue2+elementUi的案例、java访问数据库服务、java提供接口服务

文章目录 前言后端关键代码前端关键代码完整代码 前言 1、项目不使用前后端分离。 2、在创建SpringBoot的时候要注意各个插件间的版本问题。 3、后端技术SpringBootMyBatisPlusMySql。 4、前端技术vue2elementUi。 后端关键代码 简单介绍 1、数据库名称ssm_db 2、表名称tbl_bo…

Ext JS之Ext Direct快速入门

Ext Direct是一个专有名词, Direct是直接的意思。 Ext Direct 是 Ext JS 框架中的一个功能模块,用于简化前端 JavaScript 应用程序与后端服务器之间的通信和数据交换。 Ext Direct 提供了一种直接的、透明的方式来调用服务器上的方法和处理服务器响应,而无需编写大量的手动…

Centos 7 安装系列(8):openGauss 3.0.0

安装依赖包&#xff1a; yum -y install libaio-devel flex bison ncurses-devel glibc-devel patch redhat-lsb-core readline-devel openssl-devel sqlite-devel libnsl 安装插件&#xff1a; yum install -y bzip2 net-tools为什么要安装这两个&#xff1f; 安装bzip2 是…

# Go学习-Day5

文章目录 map增加和更新删除查询遍历&#xff08;for-range&#xff09;map切片关于哈希表遍历的一点看法对map的key排序 结构体与OOP声明、初始化、序列化方法工厂模式 个人博客&#xff1a;CSDN博客 map map是一个key-value的数据结构&#xff0c;又称为字段或关联数组 Gol…

关于使用远程工具连接mysql数据库时,提示:Public Key Retrieval is not allowed

我在使用DBeaver工具连接 数据库时&#xff0c;提示&#xff1a;Public Key Retrieval is not allowed&#xff0c; 我在前一天还是可以连接的&#xff0c;但是今天突然无法连接了&#xff0c; 但是最后捣鼓了一下又可以了。 具体方法&#xff1a;首先先把mysql服务停了&#x…

在编辑器中使用正则

正则是一种文本处理工具&#xff0c;常见的功能有文本验证、文本提取、文本替换、文本切割等。有一些地方说的正则匹配&#xff0c;其实是包括了校验和提取两个功能。 校验常用于验证整个文本的组成是不是符合规则&#xff0c;比如密码规则校验。提取则是从大段的文本中抽取出…

5G NR:PRACH时域资源

PRACH occasion时域位置由高层参数RACH-ConfigGeneric->prach-ConfigurationIndex指示&#xff0c;根据小区不同的频域和模式&#xff0c;38.211的第6.3.3节中给出了prach-ConfigurationIndex所对应的表格。 小区频段为FR1&#xff0c;FDD模式(paired频谱)/SUL&#xff0c;…