Go 内联优化让程序员爱不释手


Posted in Golang onJune 21, 2022

前言:

这是一篇介绍 Go 编译器如何实现内联的文章,以及这种优化将如何影响你的 Go 代码。

什么是内联?

内联是将较小的函数合并到它们各自的调用者中的行为。其在不同的计算历史时期的做法不一样,如下:

  • 早期:这种优化通常是由手工完成的。
  • 现在:内联是在编译过程中自动进行的一类基本优化之一。

为什么内联很重要?

内联是很重要的,每一门语言都必然会有。

具体的原因如下:

  • 它消除了函数调用本身的开销。
  • 它允许编译器更有效地应用其他优化策略。

核心来讲,就是性能更好了。

函数调用的开销

基本知识

在任何语言中调用一个函数都是有代价的。将参数编入寄存器或堆栈(取决于ABI),并在返回时反转这一过程,这些都是开销。

调用一个函数需要将程序计数器从指令流中的一个点跳到另一个点,这可能会导致流水线停滞。一旦进入函数,通常需要一些前言来为函数的执行准备一个新的堆栈框架,在返回调用者之前,还需要一个类似的尾声来退掉这个框架。

Go 中的开销

在 Go 中,一个函数的调用需要额外的成本来支持动态堆栈的增长。在进入时,goroutine 可用的堆栈空间的数量与函数所需的数量进行比较。

如果可用的堆栈空间不足,序言就会跳转到运行时逻辑,通过将堆栈复制到一个新的、更大的位置来增加堆栈。

一旦这样做了,运行时就会跳回到原始函数的起点,再次进行堆栈检查,现在通过了,然后继续调用。通过这种方式,goroutines可以从一个小的堆栈分配开始,只有在需要时才会增加。

这种检查很便宜,只需要几条指令,而且由于goroutine的堆栈以几何级数增长,检查很少失败。因此,现代处理器中的分支预测单元可以通过假设堆栈检查总是成功来隐藏堆栈检查的成本。在处理器错误预测堆栈检查并不得不丢弃它在投机执行时所做的工作的情况下,与运行时增长goroutine堆栈所需的工作成本相比,管道停滞的成本相对较小。

Go 里的优化

虽然每个函数调用的通用组件和 Go 特定组件的开销被使用投机执行技术的现代处理器很好地优化了,但这些开销不能完全消除,因此每个函数调用都带有性能成本,超过了执行有用工作的时间。由于函数调用的开销是固定的,较小的函数相对于较大的函数要付出更大的代价,因为它们每次调用的有用工作往往较少。

因此,消除这些开销的解决方案必须是消除函数调用本身,Go 编译器在某些条件下通过用函数的内容替换对函数的调用来做到这一点。这被称为内联,因为它使函数的主体与它的调用者保持一致。

改善优化的机会

Cliff Click 博士将内联描述为现代编译器进行的优化,因为它是常量传播和死代码消除等优化的基础。

实际上,内联允许编译器看得更远,允许它在特定函数被调用的情况下,观察到可以进一步简化或完全消除的逻辑。

由于内联可以递归应用,优化决策不仅可以在每个单独的函数的上下文中做出,还可以应用于调用路径中的函数链。

进行内联优化

不允许内联

内联的效果可以通过这个小例子来证明:

package main
import "testing"
//go:noinline
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
var Result int
func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = max(-1, i)
    }
    Result = r
}

运行这个基准可以得到以下结果:

% go test -bench=. 
BenchmarkMax-4   530687617         2.24 ns/op

从执行结果来看,max(-1, i)的成本大约是 2.24ns,感觉性能不错。

允许内联

现在让我们去掉 //go:noinline pragma 的语句,再看看不允许内联的情况下,性能是否会改变。

如下结果:

% go test -bench=. 
BenchmarkMax-4   1000000000         0.514 ns/op

两个结果对比一看,2.24ns 和 0.51ns。差距至少一倍以上,根据 benchstat 的建议,内联情况下,性能提高了 78%。

如下结果:

% benchstat {old,new}.txt
name   old time/op  new time/op  delta
Max-4  2.21ns ± 1%  0.49ns ± 6%  -77.96%  (p=0.000 n=18+19)

这些改进从何而来?

首先,取消函数调用和相关的前导动作是主要的改进贡献者。其将 max 函数的内容拉到它的调用者中,减少了处理器执行的指令数量,并消除了几个分支。

现在 max 函数的内容对编译器来说是可见的,当它优化 BenchmarkMax 时,它可以做一些额外的改进。

考虑到一旦 max 被内联,BenchmarkMax 的主体对编译器而言就会有所改变,与用户端看到的并不一样。

如下代码:

func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        if -1 > i {
            r = -1
        } else {
            r = i
        }
    }
    Result = r
}

再次运行基准测试,我们看到我们手动内联的版本与编译器内联的版本表现一样好。

如下结果:

% benchstat {old,new}.txt
name   old time/op  new time/op  delta
Max-4  2.21ns ± 1%  0.48ns ± 3%  -78.14%  (p=0.000 n=18+18)

现在,编译器可以获得 max 内联到 BenchmarkMax 的结果,它可以应用以前不可能的优化方法。

例如:编译器注意到 i 被初始化为 0,并且只被递增,所以任何与 i 的比较都可以假定 i 永远不会是负数。因此,条件 -1 > i 将永远不会为真。

在证明了 -1 > i 永远不会为真之后,编译器可以将代码简化为:

func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        if false {  // 注意已为 false
            r = -1
        } else {
            r = i
        }
    }
    Result = r
}

并且由于该分支现在是一个常数,编译器可以消除无法到达的路径,只留下如下代码:

func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = i
    }
    Result = r
}

通过内联和它所释放的优化,编译器已经将表达式 r = max(-1, i) 简化为 r = i

这个例子非常不错,很好的体现了内联的优化过程和性能提升的缘由。

内联的限制

在这篇文章中,讨论了所谓的叶子内联:将调用栈底部的一个函数内联到其直接调用者中的行为。

内联是一个递归的过程,一旦一个函数被内联到它的调用者中,编译器就可能将产生的代码内联到它的调用者中,依此类推。

例如如下代码:

func BenchmarkMaxMaxMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = max(max(-1, i), max(0, i))
    }
    Result = r
}

该运行速度将会和前面的例子一样快,因为编译器能够反复应用上面的优化,将代码减少到相同的 r = i 表达式。

总结

这篇文章针对内联进行了基本的概念介绍和分析,并且通过 Go 的例子进行了一步步的剖析,让大家对真实案例有了一个更贴切的理解。

Go 编译器的优化总是无处不在的。

到此这篇关于Go 内联优化让程序员爱不释手的文章就介绍到这了,更多相关Go 内联优化内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
go语言中json数据的读取和写出操作
Apr 28 Golang
彻底理解golang中什么是nil
Apr 29 Golang
go类型转换及与C的类型转换方式
May 05 Golang
golang gopm get -g -v 无法获取第三方库的解决方案
May 05 Golang
GoLang中生成UUID唯一标识的实现
May 08 Golang
Golang二维数组的使用方式
May 28 Golang
go语言使用Casbin实现角色的权限控制
Jun 26 Golang
go select编译期的优化处理逻辑使用场景分析
Jun 28 Golang
golang内置函数len的小技巧
Jul 25 Golang
Go语言实现一个简单的并发聊天室的项目实战
Mar 18 Golang
Go web入门Go pongo2模板引擎
May 20 Golang
Go gRPC进阶教程gRPC转换HTTP
Jun 16 Golang
GoFrame框架数据校验之校验结果Error接口对象
Jun 21 #Golang
GoFrame基于性能测试得知grpool使用场景
Jun 21 #Golang
Golang gRPC HTTP协议转换示例
Go Grpc Gateway兼容HTTP协议文档自动生成网关
Jun 16 #Golang
Go gRPC进阶教程gRPC转换HTTP
Jun 16 #Golang
GoFrame gredis缓存DoVar Conn连接对象 自动序列化GoFrame gredisDo/DoVar方法Conn连接对象自动序列化/反序列化总结
Jun 14 #Golang
Go调用Rust方法及外部函数接口前置
You might like
十天学会php之第九天
2006/10/09 PHP
Windows下wamp php单元测试工具PHPUnit安装及生成日志文件配置方法
2018/05/28 PHP
PHPstorm激活码2020年5月13日亲测有效
2020/09/17 PHP
Ajax一统天下之Dojo整合篇
2007/03/24 Javascript
javascript 图片上传预览-兼容标准
2009/06/01 Javascript
JavaScript的document对象和window对象详解
2010/12/30 Javascript
JavaScript 匿名函数(anonymous function)与闭包(closure)
2011/10/04 Javascript
JavaScript表达式:URL 协议介绍
2013/03/10 Javascript
firefox浏览器不支持innerText的解决方法
2013/08/07 Javascript
js 判断浏览器使用的语言示例代码
2014/03/22 Javascript
纯JS代码实现气泡效果
2016/05/04 Javascript
深入分析javascript中的错误处理机制
2016/07/17 Javascript
AngularJS 单元测试(一)详解
2016/09/21 Javascript
AngularJS 获取ng-repeat动态生成的ng-model值实例详解
2016/11/29 Javascript
javascript实现导航栏分页效果
2019/06/27 Javascript
JS实现网页时钟特效
2020/03/25 Javascript
使用纯前端JavaScript实现Excel导入导出方法过程详解
2020/08/07 Javascript
javascript实现打砖块小游戏(附完整源码)
2020/09/18 Javascript
JavaScript实现京东快递单号查询
2020/11/30 Javascript
在Python中执行系统命令的方法示例详解
2017/09/14 Python
Python实现的tcp端口检测操作示例
2018/07/24 Python
使用Python检测文章抄袭及去重算法原理解析
2019/06/14 Python
Django框架安装方法图文详解
2019/11/04 Python
sklearn线性逻辑回归和非线性逻辑回归的实现
2020/06/09 Python
Django正则URL匹配实现流程解析
2020/11/13 Python
PyCharm Ctrl+Shift+F 失灵的简单有效解决操作
2021/01/15 Python
canvas离屏技术与放大镜实现代码示例
2018/08/31 HTML / CSS
世界上最大的曲棍球商店:Pro Hockey Life
2017/10/30 全球购物
阿根廷在线宠物商店:Puppis
2018/03/23 全球购物
电子狗项圈:eDog Australia
2019/12/04 全球购物
英国现代、当代和设计师家具店:Furntastic
2020/07/18 全球购物
个人贷款授权委托书样本
2014/10/07 职场文书
2014年度思想工作总结
2014/11/27 职场文书
2015法院个人工作总结范文
2015/05/25 职场文书
docker-compose部署Yapi的方法
2022/04/08 Servers
oracle delete误删除表数据后如何恢复
2022/06/28 Oracle