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

目录
  • 前言:
  • 什么是内联?
  • 为什么内联很重要?
  • 函数调用的开销
    • 基本知识
    • Go 中的开销
    • Go 里的优化
  • 改善优化的机会
  • 进行内联优化
    • 不允许内联
    • 允许内联
  • 这些改进从何而来?
  • 内联的限制
  • 总结

前言:

这是一篇介绍 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 内联优化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go select编译期的优化处理逻辑使用场景分析

    前言 select作为Go chan通信的重要监听工具,有着很广泛的使用场景.select的使用主要是搭配通信case使用,表面上看,只是简单的select及case搭配,实际上根据case的数量及类型,在编译时select会进行优化处理,根据不同的情况调用不同的底层逻辑. select的编译处理 select编译时的核心处理逻辑如下: func walkselectcases(cases *Nodes) []*Node { ncas := cases.Len() sellineno := li

  • Django serializer优化类视图的实现示例

    一. create优化 在serializer序列化中,我们通过创建序列化器对象的方式地简化了视图函数的代码,前端传入的数据通过反序列化操作进行了各种数据校验,代码如下: from django.http import JsonResponse from django.views import View import json from .models import Project from .serializers import ProjectsSerializer class Project

  • 详解Django中views数据查询使用locals()函数进行优化

    优化场景 利用视图函数(views)查询数据之后可以通过上下文context.字典.列表等方式将数据传递给HTML模板,由template引擎接收数据并完成解析.但是通过context传递数据可能就存在在不同的视图函数中使用重复的查询语句,所以可以通过将重复查询语句设置全局变量,配合locals()函数进行数据查询与传递. 优化前 def index(request): threatname = '威胁情报展示' url = 'www.testtip.com' allthreat = Threa

  • MongoDB数据库安装部署及警告优化

    目录 1.软件下载 2.部署MongoDB 2.1.规划部署目录 2.2.下载软件包 2.3.安装MongoDB 2.4.MongoDB配置文件介绍 2.5.编写MongoDB配置文件 2.6.启动MongoDB 2.7.如何关闭MongoDB 2.8.登录MongoDB 3.优化MongoDB警告信息 3.1.优化启动用户警告 3.2.优化大内存页警告 3.2.1.永久关闭大内存页 3.2.2.临时关闭大内存页 3.3.优化limit警告 1.软件下载 3.6.13版本:https://fas

  • Django项目优化数据库操作总结

    目录 合理的创建索引 设置数据库持久连接 减少SQL的执行次数 仅获取需要的字段数据 使用批量创建.更新和删除,不随意对结果排序 参考网址:Django官方数据库优化 使用 QuerySet.explain() 来了解你的数据库是如何执行特定的 QuerySet 的. 你可能还想使用一个外部项目,比如 django-debug-toolbar ,或者一个直接监控数据库的工具. 合理的创建索引 索引可能有助于加快查询速度,但是也要注意索引会占用磁盘空间,创建不必要的索引只会形成浪费.数据库表中的主

  • 浅谈优化Django ORM中的性能问题

    Django是个好工具,使用的很广泛. 在应用比较小的时候,会觉得它很快,但是随着应用复杂和壮大,就显得没那么高效了.当你了解所用的Web框架一些内部机制之后,才能写成比较高效的代码. 怎么查问题 Web系统是个挺复杂的玩意,有时候有点无从下手哈.可以采用 自底向上 的顺序,从数据存储一直到数据展现,按照这个顺序一点一点查找性能问题. 数据库 (缺少索引/数据模型) 数据存储接口 (ORM/低效的查询) 展现/数据使用 (Views/报表等) Web应用的大部分问题都会跟 数据库 扯上关系.除非

  • GoFrame代码优化gconv类型转换避免重复定义map

    目录 前言 核心重点 优化前 优化后: 可以这么写的原因 进一步优化 批量写入 更优雅的写法如下 总结 前言 最近一直在研究 GoFrame 框架,经过一段时间的使用.总结.思考,发现确实不失为一款非常值得使用的企业级开发框架. 在我初识GoFrame教程后,曾整理过一篇文章: 非常适合PHP同学学习的GO框架:GoFrame,有兴趣的同学可以阅读一下. 今天重点讲一下我使用GoFrame的代码优化之旅. 核心重点 GoFrame几乎封装了所有能封装的东西,而我们需要做的就是在框架的基础上约定好

  • Django程序的优化技巧

    友情提示: 过度性能优化是没有必要甚至有害的,因为花大力气带来的毫秒级的响应提升你的用户可能根本感知不到,毕竟开发人员的时间也很宝贵. 性能优化指标 在对一个Web项目进行性能优化时,我们通常需要评价多个指标: 响应时间 最大并发连接数 代码的行数 函数调用次数 内存占用情况 CPU占比 其中响应时间(服务器从接收用户请求,处理该请求并返回结果所需的总的时间)通常是最重要的指标,因为过长的响应时间会让用户厌倦等待,转投其它网站或APP.当你的用户数量变得非常庞大,如何提高最大并发连接数,减少内存

  • python3 googletrans超时报错问题及翻译工具优化方案 附源码

    一. 问题: 在写调用谷歌翻译接口的脚本时,老是报错,我使用的的是googletrans这个模块中Translator的translate方法,程序运行以后会报访问超时错误: Traceback (most recent call last): File "E:/PycharmProjects/MyProject/Translate/translate_test.py", line 3, in <module> result=translator.translate('안녕

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

    目录 前言: 什么是内联? 为什么内联很重要? 函数调用的开销 基本知识 Go 中的开销 Go 里的优化 改善优化的机会 进行内联优化 不允许内联 允许内联 这些改进从何而来? 内联的限制 总结 前言: 这是一篇介绍 Go 编译器如何实现内联的文章,以及这种优化将如何影响你的 Go 代码. 什么是内联? 内联是将较小的函数合并到它们各自的调用者中的行为.其在不同的计算历史时期的做法不一样,如下: 早期:这种优化通常是由手工完成的. 现在:内联是在编译过程中自动进行的一类基本优化之一. 为什么内联

  • C++类与对象深入之引用与内联函数与auto关键字及for循环详解

    目录 一:引用 1.1:概念 1.2:引用特性 1.3:常引用 1.4:使用场景 1.5:引用和指针的区别 二:内联函数 2.1:概念 2.2:特性 2.3:面试题 三:auto关键字 3.1:auto简介 3.2:auto使用细则 3.3:auto不能推导的场景 四:基于范围的for循环 4.1:范围for循环的语法 4.2:范围for循环的使用条件 一:引用 1.1:概念 引用不是定义一个新的变量,而是给已经存在的变量取一个别名.注意:编译器不会给引用变量开辟内存空间,他和他的引用变量共用同

  • C++的内联函数你了解吗

    目录 1.概念 2.函数演示 3.函数特性 总结 1.概念 以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率. 2.函数演示 我们先来看一下普通的函数: #include <iostream> using namespace std; int Add(int left, int right) { return left + right; } int main() { Add(1, 2); return 0; }

  • C++ 引用与内联函数详情

    目录 引用初阶 什么是引用 为何要有引用 引用指向同一块空间 引用的特性 定义时必须初识化 一个变量可以多次引用 引用一旦引用了一个实例,不能在再引用其他的实例 引用进阶 常引用 权限 临时变量具有常属性 引用的场景 做参数 返回值 引用做返回值 引用不会开辟空间 引用和指针比较 内联函数 为何存在 内联函数 展开短小的函数 内联函数的特性 较大的函数编译器不会发生内联 声明定义一起 引用初阶 引用是C++的特性的之一,不过C++没有没有给引用特意出一个关键字,使用了操作符的重载.引用在C++中

  • C++深入探索内联函数inline与auto关键字的使用

    目录 1.内敛函数 1.1问题引入 1.2内联函数的概念 1.3内敛函数的特性 2.auto关键字 2.1 auto简介 2.2 auto的使用细则 2.3 auto不能推导的场景 2.4 auto与新式for循环使用 1.内敛函数 1.1问题引入 我们在使用C语言中我们都学过函数,我们知道函数在调用的过程中需要开辟栈帧.如果我们需要频繁的调用一个函数,假设我们调用10次Add()函数,那我们就需要建立10次栈帧.我们都知道在栈帧中要做很多事情,例如保存寄存器,压参数,压返回值等等,这个过程是很

  • Kotlin中关于内联函数的一些理解分享

    前言 看了很多博客,才明白了内联的含义,其实最根本的就是将写在别处的代码拷贝到你现在执行的方法中,相当于在一个方法中执行,java的方法执行是需要压栈出栈的对吧,如果是两三个方法那就是两三次的压栈出栈,为了节省这个操作,提高一定的效率,kotlin就出了这么个函数.但又想想,如果是个超级大的函数,考来考去的也是很麻烦啊,所以这东西需要自己权衡吧,遵守单一职责,降低代码圈发杂度才是根本. 内联函数的理解 inline函数(内联函数)从概念上讲是编译器使用函数实现的真实代码来替换每一次的函数调用,带

  • C++入门(命名空间,缺省参数,函数重载,引用,内联函数,auto,范围for)

    一.C++关键字 C++总共有63个关键字,在入门阶段我们只是大致了解一下就可,在后续博客中会逐渐讲解 二.命名空间 相信学过C++的同学,一定都写过下面这个简单的程序 #include<iostream> using namespace std; int main() { cout<<"hello world"<<endl; return 0; } 我们先来看第二行代码,using namespace std , 这行代码是什么意思呢 ? 这里我们

  • C#程序员应该养成的程序性能优化写法

    曾经在网上听过这样一句话 程序的可读性和性能是成反比的 我非常赞同这句话,所以对于那些极度影响阅读的性能优化我就不在这里赘述了 今天主要说的就是一些举手之劳即可完成的性能优化 减少重复代码 这是最基本的优化方案,尽可能减少那些重复做的事,让他们只做一次 比较常见是这种代码,同样的Math.Cos(angle) 和Math.Sin(angle)都做了2次 优化前 private Point RotatePt(double angle, Point pt) { Point pRet = new Po

  • 90%程序员面试会遇到的索引优化问题

    前言 本文给大家分享了90%程序员面试都用得上的索引优化,重点提一下,索引基本原理和创建索引的原则是重点,面试基本必问!大家可以收藏好多理解理解.下面来一起看看详细的介绍吧. 关于索引,分为以下几点来讲解(技术文): 索引的概述(什么是索引,索引的优缺点) 索引的基本使用(创建索引) 索引的基本原理(面试重点) 索引的数据结构(B树,hash) 创建索引的原则(重中之重,面试必问!敬请收藏!) 百万级别或以上的数据如何删除 一.索引的概述 1)什么是索引? 索引是一种特殊的文件(InnoDB数据

  • Java程序员编程性能优化必备的34个小技巧(总结)

    1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 控制资源的使用,通过线程同步来控制资源的并发访问: 控制实例的产生,以达到节约资源的目的: 控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信. 2.尽量避免随意使用静态变量 要知道,当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如: 此时静态变量b的生命周期与A类同步,如

随机推荐