Golang 的 1.13 版本 与 1.14 版本对 defer 进行了两次优化,使得 defer 的性能开销在大部分场景下都得到大幅降低,其中到底经历了什么原理?
这是因为这两个版本对 defer 各加入了一项新的机制,使得 defer 语句在编译时,编译器会根据不同版本与情况,对每个 defer 选择不同的机制,以更轻量的方式运行调用。
在 Golang 1.13 之前的版本中,所有 defer 都是在堆上分配,该机制在编译时会进行两个步骤:
这种机制的主要性能问题存在于每个 defer 语句产生记录时的内存分配,以及记录参数和完成调用时参数移动的系统调用开销。
Go 1.13 版本新加入 deferprocStack 实现了在栈上分配的形式来取代 deferproc ,相比后者,栈上分配在函数返回后 _defer 便得到释放,省去了内存分配时产生的性能开销,只需适当维护 _defer 的链表即可。
编译器有自己的逻辑去选择使用 deferproc 还是 deferprocStack ,大部分情况下都会使用后者,性能会提升约 30%。不过在 defer 语句出现在了循环语句里,或者无法执行更高阶的编译器优化时,亦或者同一个函数中使用了过多的 defer 时,依然会使用 deferproc 。
Go 1.14 版本继续加入了开发编码(open coded),该机制会将延迟调用直接插入函数返回之前,省去了运行时的 deferproc 或 deferprocStack 操作,在运行时的 deferreturn 也不会进行尾递归调用,而是直接在一个循环中遍历所有延迟函数执行。
这种机制使得 defer 的 开销几乎可以忽略 ,唯一的运行时成本就是存储参与延迟调用的相关信息,不过使用此机制需要一些条件:
-gcflags "-N"
defer
defer
该机制还引入了一种元素 —— 延迟比特(defer bit) ,用于运行时记录每个 defer 是否被执行(尤其是在条件判断分支中的 defer ),从而便于判断最后的延迟调用该执行哪些函数。
延迟比特的原理:
同一个函数内每出现一个 defer 都会为其分配 1 个比特,如果被执行到则设为 1,否则设为 0,当到达函数返回之前需要判断延迟调用时,则用掩码判断每个位置的比特,若为 1 则调用延迟函数,否则跳过。
为了轻量,官方将延迟比特限制为 1 个字节,即 8 个比特,这就是为什么不能超过 8 个 defer 的原因,若超过依然会选择堆栈分配,但显然大部分情况不会超过 8 个。
用代码演示如下:
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,不调用 f2
if deferBits & 1<<1 != 0 {
deferBits &^= 1<<1 // 移位为下次判断准备
_f2(_a2)
}
// 同理,由于 00000001 & 00000001 != 0,调用 f1
if deferBits && 1<<0 != 0 {
deferBits &^= 1<<0
_f1(_a1)
}
以往 Golang defer 语句的性能问题一直饱受诟病,最近正式发布的 1.14 版本终于为这个争议画上了阶段性的句号。如果不是在特殊情况下,我们不需要再计较 defer 的性能开销。
原文 http://www.cnblogs.com/zkqiang/p/12389420.html
如果让你列出过去十年出现的重要编程语言,几乎可以肯定Go和Rust榜上有名。同样,如果让你坐下来想想如今哪种编程语言最适合开发对微服务友好且安全的框架或应用程序,你可能发觉在Go和Rust之间难以决定。
在使用 Go 编写复杂的服务时,您将遇到一个典型的主题是中间件。这个话题在网上被讨论了一次又一次。本质上,中间件允许我们做了如下事情:这些与 express.js 中间件所做的工作非常类似。
新学习go语言的人可能遇到常见的错误,其中有两个比较常见的错误,需要单独拿出来说下,为什么要单独说呢,因为这两个错误跟其他语言不同,是因为go本身的设计造成的。
在用 Go 编程时,总会遇到各种奇奇怪怪的错误,国内外已经有许多小伙伴总结过(参考链接见参考),感觉都能凑一桌了。希望对大家有所帮助。
本文将介绍展示如何使用 Go 编程语言 来复制文件。在 Go 中复制文件的方法有很多,我只介绍三种最常见的:使用 Go 库中的 io.Copy() 函数调用、一次读取输入文件并将其写入另一个文件
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!