前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >探究 Go 语言 defer 语句的三种机制

探究 Go 语言 defer 语句的三种机制

作者头像
张凯强
发布于 2020-03-11 04:46:28
发布于 2020-03-11 04:46:28
84800
代码可运行
举报
文章被收录于专栏:面向人生编程面向人生编程
运行总次数:0
代码可运行

Golang 的 1.13 版本 与 1.14 版本对 defer 进行了两次优化,使得 defer 的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理?

这是因为这两个版本对 defer 各加入了一项新的机制,使得 defer 语句在编译时,编译器会根据不同版本与情况,对每个 defer 选择不同的机制,以更轻量的方式运行调用。

堆上分配

在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配,该机制在编译时会进行两个步骤:

1.在 defer 语句的位置插入 runtime.deferproc,当被执行时,延迟调用会被保存为一个 _defer 记录,并将被延迟调用的入口地址及其参数复制保存,存入 Goroutine 的调用链表中。2.在函数返回之前的位置插入 runtime.deferreturn,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行。

这种机制的主要性能问题存在于每个 defer 语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。

栈上分配

Go 1.13 版本新加入 deferprocStack 实现了在栈上分配的形式来取代 deferproc,相比后者,栈上分配在函数返回后 _defer 便得到释放,省去了内存分配时产生的性能开销,只需适当维护 _defer 的链表即可。

编译器有自己的逻辑去选择使用 deferproc 还是 deferprocStack,大部分情况下都会使用后者,性能会提升约 30%。不过在 defer 语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的 defer 时,依然会使用 deferproc

开放编码

Go 1.14 版本继续加入了开发编码(open coded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的 deferprocdeferprocStack 操作,在运行时的 deferreturn 也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。

这种机制使得 defer开销几乎可以忽略,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用此机制需要一些条件:

1.没有禁用编译器优化,即没有设置 -gcflags "-N";2.函数内 defer 的数量不超过 8 个,且返回语句与延迟语句个数的乘积不超过 15;3.defer 不是在循环语句中。

该机制还引入了一种元素 —— 延迟比特(defer bit),用于运行时记录每个 defer 是否被执行(尤其是在条件判断分支中的 defer),从而便于判断最后的延迟调用该执行哪些函数。

延迟比特的原理:同一个函数内每出现一个 defer 都会为其分配 1 个比特,如果被执行到则设为 1,否则设为 0,当到达函数返回之前需要判断延迟调用时,则用掩码判断每个位置的比特,若为 1 则调用延迟函数,否则跳过。

为了轻量,官方将延迟比特限制为 1 个字节,即 8 个比特,这就是为什么不能超过 8 个 defer 的原因,若超过依然会选择堆栈分配,但显然大部分情况不会超过 8 个。

用代码演示如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
deferBits = 0  // 延迟比特初始值 00000000
deferBits |= 1 << 0  // 执行第一个 defer,设置为 00000001_f1 = f1  // 延迟函数_a1 = a1  // 延迟函数的参数if cond {    // 如果第二个 defer 被执行,则设置为 00000011,否则依然为 00000001    deferBits |= 1 << 1    _f2 = f2    _a2 = a2}...exit:// 函数返回之前,倒序检查延迟比特,通过掩码逐位进行与运算,来判断是否调用函数
// 假如 deferBits 为 00000011,则 00000011 & 00000010 != 0,因此调用 f2// 否则 00000001 & 00000010 == 0,不调用 f2if deferBits & 1 << 1!= 0{    deferBits &^= 1 << 1  // 移位为下次判断准备    _f2(_a2)}// 同理,由于 00000001 & 00000001 != 0,调用 f1if deferBits && 1 << 0!= 0{    deferBits &^= 1 << 0    _f1(_a1)}

总结

以往 Golang defer 语句的性能问题一直饱受诟病,最近正式发布的 1.14 版本终于为这个争议画上了阶段性的句号。如果不是在特殊情况下,我们不需要再计较 defer 的性能开销。

参考资料

[1] Ou Changkun - Go 语言原本: https://changkun.de/golang/zh-cn/part2runtime/ch09lang/defer/

[2] 峰云就她了 - go1.14实现defer性能大幅度提升原理: http://xiaorui.cc/archives/6579

[3] 34481-opencoded-defers: https://github.com/golang/proposal/blob/master/design/34481-opencoded-defers.md

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 面向人生编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
万字长文:从实践到原理说透Golang defer
Go 提供关键字defer 处理延迟调用问题。在语法上,defer与普通的函数调用没有什么区别。正如官方文档描述的那样:
aneutron
2022/12/18
9590
万字长文:从实践到原理说透Golang defer
go语言-defer关键字
首先要明确的是:defer是在return之前执行的。这个在 官方文档中明确说明了的。然后是了解defer的实现方式,大致就是在defer出现的地方,插入指令CALL runtime.deferproc,然后在函数返回之前的地方,插入指令CALL runtime.deferreturn。为了支持多值返回,go是用栈返回值的
MaybeHC
2024/04/23
750
深入分析Go defer底层原理
目前的Go defer的源码分析文章很多都绕过了最为复杂的编译器优化阶段,而且对开放编码方式实现defer关键字的原理解释的不够清楚,本文尝试啃下defer在编译器(gc, go compiler)优化阶段的这个硬骨头,并给出defer在堆上分配、栈上分配、开放编码三种实现方式的编译期和运行时的完整的执行过程。
涂明光
2022/10/17
2.1K1
深入 Go 语言 defer 实现原理
在上面的例子中,使用 for 循环将字符串 Naveen进行遍历后调用 defer,这些 defer调用仿佛就像被压栈一样,最后被推入堆栈的defer调用将被拉出并首先执行。
luozhiyun
2021/05/01
8070
详解defer实现机制(附上三道面试题,我不信你们都能做对)
我们首先来看一看defer关键字是怎么使用的,一个经典的场景就是我们在使用事务时,发生错误需要回滚,这时我们就可以用使用defer来保证程序退出时保证事务回滚,示例代码如下:
Golang梦工厂
2022/07/08
4800
详解defer实现机制(附上三道面试题,我不信你们都能做对)
defer 的前世今生
延迟语句 defer 在最早期的 Go 语言设计中并不存在,后来才单独增加了这一特性,由 Robert Griesemer 完成语言规范的编写 [Griesemer, 2009], 并由 Ken Thompson 完成最早期的实现 [Thompson, 2009],两人合作完成这一语言特性。
梦醒人间
2020/04/02
1.1K0
defer 的前世今生
Go 中 defer 关键字原理
runtime.deferproc 负责注册, runtime.deferreturn 负责执行。
王小明_HIT
2021/08/31
6800
深入理解defer(下)defer实现机制
上一篇文章我们主要从使用的角度介绍了 defer 的基础知识,本文我们来分析一下 defer 的实现机制。
阿波张
2019/06/24
8461
深入理解defer(下)defer实现机制
深入理解defer(上)defer基础
f() 函数首先通过调用 getResource() 获取了某种资源(比如打开文件,加锁等),然后进行了一些我们不太关心的操作,但这些操作可能会导致 f() 函数提前返回,为了避免资源泄露,所以每个 return 之前都调用了 r.release() 函数对资源进行释放。这段代码看起来并不糟糕,但有两个小问题:代码臃肿和可维护性比较差。臃肿倒是其次,主要问题在于代码的可维护性差,因为随着开发和维护的进行,修改代码在所难免,一旦对 f() 函数进行修改添加某个提前返回的分支,就很有可能在提前 return 时忘记调用 r.release() 释放资源,从而导致资源泄漏。
阿波张
2019/06/24
5580
Go高阶指南07,一文搞懂 defer 实现原理
defer 语句用于延迟函数的调用,使用 defer 关键字修饰一个函数,会将这个函数压入栈中,当函数返回时,再把栈中函数取出执行。
微客鸟窝
2021/09/10
1.1K0
<Go语言学习笔记>【异常处理】
通常 panic 和 recover 是用来处理异常问题的。我们来综述下,他们各自的特点:
Porco1Rosso
2020/11/19
1.6K0
<Go语言学习笔记>【异常处理】
GO 中 defer的实现原理
要是对 chan 通道还有点兴趣的话,欢迎查看文章 GO 中 Chan 实现原理分享
阿兵云原生
2023/02/16
4310
Go defer 会有性能损耗,尽量不要用?
从结果上来,使用 defer 后的函数开销确实比没使用高了不少,这损耗用到哪里去了呢?
sunsky
2020/08/20
1.1K0
golang的defer使用相关
deferFunc1 返回的是1 deferFunc2返回的是2 deferFunc3返回的是改变后的数组,deferFunc4返回的倒序的数据。
公众号-利志分享
2022/04/25
2200
defer 原理分析
很早之前我有写过有关 defer 的博客,现在看来起标题的时候有点蠢,有点标题党,(https://www.linkinstars.com/post/48e6221e.html) 其中主要是注重与 defer 的使用,避免使用上的问题,对于 defer 具体实现其实只是点了一下,而今天就让我们详细看看 defer 究竟是如何实现的。
LinkinStar
2022/09/01
4950
不是我吹,你可能连defer都不清楚
在golang中,对于defer,我之前的理解就是和java中的finally代码块一样,没什么难度,但是吧,当我最近看的一些神奇的问题,我就发现原来并非想的那么简单。
LinkinStar
2022/09/01
2530
2020-11-19:go中,defer原理是什么?
福哥答案2020-11-19: undefined评论,有好几个参考地址 什么是defer defer是go语言提供的一种用于注册延迟调用的机制:让函数或者语句在当前函数执行完毕(包括return正常结束或者panic导致的异常结束)之后执行。 defer语句通常用于一些成对的操作场景,打开/关闭连接,加锁/解锁,打开文件/关闭文件等等 defer在一些需要回收资源的场景中非常有用 为什么需要defer 有效防止内存泄漏 defer底层原理 每次defer语句在执行的时候,都会将函数进行“压栈”,函数参数
福大大架构师每日一题
2020/11/19
6730
Golang 语言中的 defer 怎么使用?
在 Golang 语言中,我们可以在函数(自定义和部分内置)或方法中使用 defer 关键字注册延迟调用(一个或多个),多个延迟调用的执行顺序是先进后出(FILO)。并且不会受到函数执行结束退出,显式调用 return 和主动(或被动)触发 panic 的影响,注册成功的所有延迟调用都会被执行,除非 defer 注册在 return 之后或者函数(或方法)调用 os.Exit(1)。
frank.
2021/06/22
5020
探究 Go 源码中 panic & recover 有哪些坑?
写这一篇文章的原因是最近在工作中有位小伙伴在写代码的时候直接用 Go 关键字起了一个 Goroutine,然后发生了空指针的问题,由于没有 recover 导致了整个程序宕掉的问题。代码类似这样:
luozhiyun
2021/11/24
1.3K0
探究 Go 源码中 panic & recover 有哪些坑?
go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数
> 运行结果: 3 2 1 . > > 「雪之梦技术驿站」: defer fmt.Println(1) 和 defer fmt.Println(2) 两个语句由于前面存在 defer 关键字,因此均被延迟到正常语句 return 前.当多个 defer 语句均被延迟时,倒序执行延迟语句,这种特点非常类似于数据结构的栈(先入后出).所以依次输出 fmt.Println(3) ,defer fmt.Println(2) ,defer fmt.Println(1) .
雪之梦技术驿站
2019/11/20
5780
相关推荐
万字长文:从实践到原理说透Golang defer
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验