Go语言设计与实现 -- 栈空间管理

news2024/11/29 16:36:02

寄存器

stack-registers

图片来自于面向信仰编程

Go 语言的汇编代码包含 BP 和 SP 两个栈寄存器,它们分别存储了栈的基址指针和栈顶的地址,栈内存与函数调用的关系非常紧密,我们在函数调用一节中曾经介绍过栈区,BP 和 SP 之间的内存就是当前函数的调用栈。

线程栈

线程和进程都是代码执行的上下文,但是如果一个应用程序包含成百上千个执行上下文并且每个上下文都是线程,会占用大量的内存空间并带来其他的额外开销,Go 语言在设计时认为执行上下文是轻量级的,所以它在用户态实现 Goroutine 作为执行上下文。

逃逸分析

在 C 语言和 C++ 这类需要手动管理内存的编程语言中,将对象或者结构体分配到栈上或者堆上是由工程师自主决定的,这也为工程师的工作带来的挑战,如果工程师能够精准地为每一个变量分配合理的空间,那么整个程序的运行效率和内存使用效率一定是最高的,但是手动分配内存会导致如下的两个问题:

  1. 不需要分配到堆上的对象分配到了堆上 — 浪费内存空间;
  2. 需要分配到堆上的对象分配到了栈上 — 悬挂指针、影响内存安全;

与悬挂指针相比,浪费内存空间反而是小问题。在 C 语言中,栈上的变量被函数作为返回值返回给调用方是一个常见的错误,在如下所示的代码中,栈上的变量 i 被错误返回:

int *dangling_pointer() {
    int i = 2;
    return &i;
}

dangling_pointer 函数返回后,它的本地变量会被编译器回收,调用方获取的是危险的悬挂指针,我们不确定当前指针指向的值是否合法时,这种问题在大型项目中是比较难以发现和定位的。

在编译器优化中,逃逸分析是用来决定指针动态作用域的方法。Go 语言的编译器使用逃逸分析决定哪些变量应该在栈上分配,哪些变量应该在堆上分配,其中包括使用 newmake 和字面量等方法隐式分配的内存,Go 语言的逃逸分析遵循以下两个不变性:

  1. 指向栈对象的指针不能存在于堆中;
  2. 指向栈对象的指针不能在栈对象回收后存活;

escape-analysis-and-key-invariants

我们通过上图展示两条不变性存在的意义,当我们违反了第一条不变性时,堆上的绿色指针指向了栈中的黄色内存,一旦函数返回后函数栈会被回收,该绿色指针指向的值就不再合法;如果我们违反了第二条不变性,因为寄存器 SP 下面的内存由于函数返回已经释放,所以黄色指针指向的内存已经不再合法。

逃逸分析是静态分析的一种,在编译器解析了 Go 语言源文件后,它可以获得整个程序的抽象语法树(Abstract syntax tree,AST),编译器可以根据抽象语法树分析静态的数据流,我们会通过以下几个步骤实现静态分析的全过程:

  1. 构建带权重的有向图,其中顶点 cmd/compile/internal/gc.EscLocation 表示被分配的变量,边 cmd/compile/internal/gc.EscEdge表示变量之间的分配关系,权重表示寻址和取地址的次数;
  2. 遍历对象分配图并查找违反两条不变性的变量分配关系,如果堆上的变量指向了栈上的变量,那么该变量需要分配在堆上;
  3. 记录从函数的调用参数到堆以及返回值的数据流,增强函数参数的逃逸分析;

决定变量是在栈上还是堆上虽然重要,但是这是一个定义相对清晰的问题,我们可以通过编译器统一作决策。为了保证内存的绝对安全,编译器可能会将一些变量错误地分配到堆上,但是因为堆也会被垃圾收集器扫描,所以不会造成内存泄露以及悬挂指针等安全问题,解放了工程师的生产力。

栈空间内存

分段栈

当 Goroutine 调用的函数层级或者局部变量需要的越来越多时,运行时会调用 runtime.morestack:go1.2runtime.newstack:go1.2创建一个新的栈空间,这些栈空间虽然不连续,但是当前 Goroutine 的多个栈空间会以链表的形式串联起来,运行时会通过指针找到连续的栈片段:

segmented-stacks

图 7-45 分段栈的内存布局

一旦 Goroutine 申请的栈空间不在被需要,运行时会调用 runtime.lessstack:go1.2runtime.oldstack:go1.2释放不再使用的内存空间。

分段栈机制虽然能够按需为当前 Goroutine 分配内存并且及时减少内存的占用,但是它也存在两个比较大的问题:

  1. 如果当前 Goroutine 的栈几乎充满,那么任意的函数调用都会触发栈扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split)
  2. 一旦 Goroutine 使用的内存越过了分段栈的扩缩容阈值,运行时会触发栈的扩容和缩容,带来额外的工作量

连续栈

连续栈可以解决分段栈中存在的两个问题,其核心原理是每当程序的栈空间不足时,初始化一片更大的栈空间并将原栈中的所有值都迁移到新栈中,新的局部变量或者函数调用就有充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤:

  • 在内存空间中分配更大的栈内存空间
  • 将旧栈中的所有内容复制到新栈中
  • 将旧栈对应变量的指针重新指向新栈
  • 销毁并回收旧栈的内存空间

在扩容过程中,最重要的是第三步,这一步能够保证指向栈的指针的正确性,因为栈中的所有变量内存都会发生变化,所以原本指向栈中变量的指针也需要调整。我们在前面提到过经过逃逸分析的 Go 语言程序的遵循以下不变性 —— 指向栈对象的指针不能存在于堆中,所以指向栈中变量的指针只能在栈上,我们只需要调整栈中的所有变量就可以保证内存的安全了。

continuous-stacks

因为需要拷贝变量和调整指针,连续栈增加了栈扩容时的额外开销,但是通过合理栈缩容机制就能避免热分裂带来的性能问题,在 GC 期间如果 Goroutine 使用了栈内存的四分之一,那就将其内存减少一半,这样在栈内存几乎充满时也只会扩容一次,不会因为函数调用频繁扩缩容。

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

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

相关文章

数智化转型进入“精装时代”,容联云助力千行百业加速上云用数赋智

随着产业数字化向前推进,企业引入数字技术的需求和热情十分充足,但要把技术下沉到市场中,还存在一个关键的矛盾:交付能力。千行百业,尤其是传统实体经济从业者,对数智化所需要的5G、IOT、AI、大数据、云计算…

canvas入门教学(5)运动小球屏保特效与下雪特效渲染

本节我们来学习两个例子,第一个例子是如下图这样的,全屏各色各样的小球随机运动,碰撞到屏幕边缘再反弹回来的特效,我们一步一步带着大家来学习这个canvas应用。 首先呢,基于上一个教程的例子,我们需要基础的构建圆, 上节教程在这里 并且呢我们要重复的多次的构建半径…

OVN实验----L2互通

概述 尽量少贴概念,只同步一些必要的名词。 central: 可以看做中心节点,central节点组件包括OVN/CMS plugin、OVN Northbound DB、ovn-northd、OVN Southbound DB。 hypervisor: 可以看做工作节点,hypervisor节点组件包括ovn-controller、ov…

Target 塔吉特的4种商品编码

Target塔吉特共有4种商品编码:TCIN、DPCI、UPC、SKU,其中DPCI、UPC和TCIN在Target系统中是唯一的ID。在target.com中查看商品时,在任一个商品中下拉进入到商品详情页(Item/Detail/Specifications)中都可以看到该商品的…

13_5、Java的IO流之转换流的使用

一、转换流涉及到的类:都是字符流InputStreamReader:将输入的字节流转换为输入的字符流。解码:字节、字节数组 ————>字符串、字符数组OutputStreamWrite:将输出的字符流转换为输出的字节流。编码:字符串、字符数…

Linux 网络探测和安全审核工具 nmap 应用实践

对于 nmap,相信很多安全运维人员并不陌生,它曾经在电影《黑客帝国》中出现过, 是黑客和网络安全人员经常用到的工具,本文重点介绍下此工具的实现原理和使用技巧。 nmap 和 Zenmap 简介 nmap 是一款开源免费的网络发现工具&#…

2023兔年大吉HTML,兔兔动态代码「兔了个兔」

文章目录一.2023兔年大吉HTML,兔兔动态代码「兔了个兔」1.1 资源获取和效果预览二.代码讲解(主要代码)1.1 背景加圆圈圈1.2.兔兔和提示字1.3 JavaScript控制动态一.2023兔年大吉HTML,兔兔动态代码「兔了个兔」 1.1 资源获取和效果…

如何在游戏中实现飘花和落叶效果

本文首发于微信公众号: 小蚂蚁教你做游戏。欢迎关注领取更多学习做游戏的原创教程资料,每天学点儿游戏开发知识。嗨!大家好,我是小蚂蚁。今天这篇文章分享一下如何在游戏中实现飘花和落叶的效果,在游戏背景中加入它们&…

FPGA:数字电路简介

文章目录数字电路的历史电子管时代晶体管时代半导体集成电路IC 时代IC的发展阶段EDA (Electronics Design Automation) 技术数字集成电路的分类数字集成电路的集成度分类从器件导电类型不同从器件类型不同数字电路的历史 数字电路是数字计算机和自动控制系统的基础&#xff0c…

[JavaEE初阶] 线程安全问题之内存可见性问题----volatile

读书要趁黑发早,白首不悔少当时 文章目录1. 什么是内存可见性问题2. 避免内存可见性问题-----volatile(易变的)3. 需要注意的点总结1. 什么是内存可见性问题 在线程A在读一个变量的时候,另一个线程B在修改这个变量,所以,线程A读到的值不是修改之后的,是一个未更新的值,读到的值…

先行“蜀道”, 四川农信核心系统分布式转型

作者:四川省农村信用社联合社 张朝辉 桂俊鸿 来源:《金融电子化》 随着四川省联社党委提出“合规银行、智慧银行、主力军银行”三大银行战略。作为四川省业务规模最大的银行业金融机构、全国农信系统“排头兵”的四川农信积极响应,率先于 2018 年 9 月完…

mysql磁盘io

1、磁盘的一些概念 1.1、盘片、片面 和 磁头 硬盘中一般会有多个盘片组成,每个盘片包含两个面,每个盘面都对应地有一个读/写磁头。受到硬盘整体体积和生产成本的限制,盘片数量都受到限制,一般都在5片以内。盘片的编号自下向上从…

Viper渗透框架

文章目录Viper 简介Viper 安装脚本安装手动安装切换到 root 用户执行命令Kali 安装 docker (我已经安装过了,不做演示,命令依次执行即可)安装 docker-compose设置安装目录生成安装目录,并进入安装目录生成 docker-compose.yml设置登录密码写入…

【C++常用算法】STL基础语法学习 | 拷贝算法替换算法

目录 ●copy ●replace ●replace_if ●swap ●copy 1.功能描述: 将容器内指定范围的元素拷贝到另一容器中 2.查看copy定义下底层代码的函数原型: 3.向deque容器中插入10~50五个数,将这五个数拷贝到另一个指定容器中并输出。 #include&…

【生产问题】前端接口请求报blocked:mixed-content

事故现象 客户端反馈系统无法使用。打开页面很多内容无法显示。 f12 显示很多请求都失败了。 定位问题 客户咨询 客户反馈昨天 在nginx 上面配置了https证书。导致了http 请求无法访问。 客户已经在nginx上面配置了https。即 网页端的请求会重定向到https请求上面。那为啥…

无需离开 Visual Studio 即可编写 Markdown

当您想要格式化代码但又不想牺牲易读性时,Markdown 是一个很好的解决方案。GitHub 将其用于自述文件,我们将其用作 Visual Studio 文档的标准。之前收到了不少来自开发者的反馈,大家希望在 Visual Studio 中使用 Markdown 编辑器。在最近的 V…

87、【栈与队列】leetcode ——347. 前 K 个高频元素:优先队列(小根堆)+Hash表(C++版本)

题目描述 原题链接:347. 前 K 个高频元素 一、优先队列(小根堆)Hash表 使用Hash表存nums中各元素出现次数,维护一个优先级队列,在里面存k个数,采用小根堆方式,从小到大进行排列。当存入的数多…

Vue3——第五章(响应式 API:isRef、unref、toRef、toRefs等工具函数)

一、isRef() 检查某个值是否为 ref。请注意,返回值是一个类型判定 (type predicate),这意味着 isRef 可以被用作类型守卫 二、unref() 如果参数是 ref,则返回内部值,否则返回参数本身。这是 val isRef(val) ? val.value : v…

【CPP】STL简介

​🌠 作者:阿亮joy. 🎆专栏:《吃透西嘎嘎》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉什么是 S…

李宏毅ML-机器学习基本概念简介

机器学习基本概念简介 Machine Learning 约等于 Looking for a function. Different types of functions: Regression: The function outputs a scalar. Classification: Given options(classes), the function outputs the correct one. How to find a function? > 预测本…