您的位置:首页 > 教程 > 其他脚本 > Go 内联优化让程序员爱不释手

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

2022-06-21 11:09:35 来源:易采站长站 作者:

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

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

6V5站长之家-易采站长站-Easck.Com

前言:

这是一篇介绍>

6V5站长之家-易采站长站-Easck.Com

什么是内联?

内联是将较小的函数合并到它们各自的调用者中的行为。其在不同的计算历史时期的做法不一样,如下:6V5站长之家-易采站长站-Easck.Com

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

    6V5站长之家-易采站长站-Easck.Com

    为什么内联很重要?

    内联是很重要的,每一门语言都必然会有。6V5站长之家-易采站长站-Easck.Com

    具体的原因如下:6V5站长之家-易采站长站-Easck.Com

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

      核心来讲,就是性能更好了。6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      函数调用的开销

      6V5站长之家-易采站长站-Easck.Com

      基本知识

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

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

      6V5站长之家-易采站长站-Easck.Com

      Go>

      在 Go 中,一个函数的调用需要额外的成本来支持动态堆栈的增长。在进入时,goroutine 可用的堆栈空间的数量与函数所需的数量进行比较。6V5站长之家-易采站长站-Easck.Com

      如果可用的堆栈空间不足,序言就会跳转到运行时逻辑,通过将堆栈复制到一个新的、更大的位置来增加堆栈。6V5站长之家-易采站长站-Easck.Com

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

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

      6V5站长之家-易采站长站-Easck.Com

      Go>

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

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

      6V5站长之家-易采站长站-Easck.Com

      改善优化的机会

      Cliff>

      实际上,内联允许编译器看得更远,允许它在特定函数被调用的情况下,观察到可以进一步简化或完全消除的逻辑。6V5站长之家-易采站长站-Easck.Com

      由于内联可以递归应用,优化决策不仅可以在每个单独的函数的上下文中做出,还可以应用于调用路径中的函数链。6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      进行内联优化

      6V5站长之家-易采站长站-Easck.Com

      不允许内联

      内联的效果可以通过这个小例子来证明:6V5站长之家-易采站长站-Easck.Com

      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
      }

      运行这个基准可以得到以下结果:6V5站长之家-易采站长站-Easck.Com

      % go test -bench=. 6V5站长之家-易采站长站-Easck.Com
      BenchmarkMax-4   530687617         2.24 ns/op6V5站长之家-易采站长站-Easck.Com

      从执行结果来看,max(-1, i)的成本大约是 2.24ns,感觉性能不错。6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      允许内联

      现在让我们去掉 //go:noinline> 的语句,再看看不允许内联的情况下,性能是否会改变。6V5站长之家-易采站长站-Easck.Com

      如下结果:6V5站长之家-易采站长站-Easck.Com

      % go test -bench=. 6V5站长之家-易采站长站-Easck.Com
      BenchmarkMax-4   1000000000         0.514 ns/op6V5站长之家-易采站长站-Easck.Com

      两个结果对比一看,2.24ns 和 0.51ns。差距至少一倍以上,根据 benchstat 的建议,内联情况下,性能提高了 78%。6V5站长之家-易采站长站-Easck.Com

      如下结果:6V5站长之家-易采站长站-Easck.Com

      % benchstat {old,new}.txt6V5站长之家-易采站长站-Easck.Com
      name   old time/op  new time/op  delta6V5站长之家-易采站长站-Easck.Com
      Max-4  2.21ns ± 1%  0.49ns ± 6%  -77.96%  (p=0.000 n=18+19)6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      这些改进从何而来?

      首先,取消函数调用和相关的前导动作是主要的改进贡献者。其将>

      现在 max 函数的内容对编译器来说是可见的,当它优化 BenchmarkMax 时,它可以做一些额外的改进。6V5站长之家-易采站长站-Easck.Com

      考虑到一旦 max 被内联,BenchmarkMax 的主体对编译器而言就会有所改变,与用户端看到的并不一样。6V5站长之家-易采站长站-Easck.Com

      如下代码:6V5站长之家-易采站长站-Easck.Com

      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
      }

      再次运行基准测试,我们看到我们手动内联的版本与编译器内联的版本表现一样好。6V5站长之家-易采站长站-Easck.Com

      如下结果:6V5站长之家-易采站长站-Easck.Com

      % benchstat {old,new}.txt6V5站长之家-易采站长站-Easck.Com
      name   old time/op  new time/op  delta6V5站长之家-易采站长站-Easck.Com
      Max-4  2.21ns ± 1%  0.48ns ± 3%  -78.14%  (p=0.000 n=18+18)6V5站长之家-易采站长站-Easck.Com

      现在,编译器可以获得 max 内联到 BenchmarkMax 的结果,它可以应用以前不可能的优化方法。6V5站长之家-易采站长站-Easck.Com

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

      在证明了 -1 > i 永远不会为真之后,编译器可以将代码简化为:6V5站长之家-易采站长站-Easck.Com

      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
      }

      并且由于该分支现在是一个常数,编译器可以消除无法到达的路径,只留下如下代码:6V5站长之家-易采站长站-Easck.Com

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

      通过内联和它所释放的优化,编译器已经将表达式 r = max(-1, i) 简化为 r = i6V5站长之家-易采站长站-Easck.Com

      这个例子非常不错,很好的体现了内联的优化过程和性能提升的缘由。6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      内联的限制

      在这篇文章中,讨论了所谓的叶子内联:将调用栈底部的一个函数内联到其直接调用者中的行为。6V5站长之家-易采站长站-Easck.Com

      内联是一个递归的过程,一旦一个函数被内联到它的调用者中,编译器就可能将产生的代码内联到它的调用者中,依此类推。6V5站长之家-易采站长站-Easck.Com

      例如如下代码:6V5站长之家-易采站长站-Easck.Com

      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 表达式。6V5站长之家-易采站长站-Easck.Com

      6V5站长之家-易采站长站-Easck.Com

      总结

      这篇文章针对内联进行了基本的概念介绍和分析,并且通过>

      Go 编译器的优化总是无处不在的。6V5站长之家-易采站长站-Easck.Com

      到此这篇关于Go 内联优化让程序员爱不释手的文章就介绍到这了,更多相关Go 内联优化内容请搜索易采站长站以前的文章或继续浏览下面的相关文章希望大家以后多多支持易采站长站!6V5站长之家-易采站长站-Easck.Com

      如有侵权,请联系QQ:279390809 电话:15144810328

相关文章

  • 使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    (1)业务复杂度介绍 开门见山,假设一个直播间同时500W人在线,那么1秒钟1000条弹幕,那么弹幕系统的推送频率就是: 500W * 1000条/秒=50亿条/秒 ,想想B站2019跨年晚会那次弹幕系统得是
    2020-07-08
  • golang中import cycle not allowed解决的一种思路

    golang中import cycle not allowed解决的一种思路

    发现问题 项目中碰到了一些问题,使用了指针函数的思路来解决相应问题 在实际项目中,因为两个项目互相引了对方的一些方法,导致了循环引用的错误,原本可以使用http的请求来解
    2019-11-10
  • 从go语言中找&和*区别详解

    从go语言中找&和*区别详解

    *和的区别 : 是取地址符号 , 即取得某个变量的地址 , 如 ; a*是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 . 从
    2020-06-23
  • Go语言中利用http发起Get和Post请求的方法示例

    Go语言中利用http发起Get和Post请求的方法示例

    关于 HTTP 协议 HTTP(即超文本传输协议)是现代网络中最常见和常用的协议之一,设计它的目的是保证客户机和服务器之间的通信。 HTTP 的工作方式是客户机与服务器之间的 “请求-应答
    2019-11-10
  • golang如何实现mapreduce单进程版本详解

    golang如何实现mapreduce单进程版本详解

    前言 MapReduce作为hadoop的编程框架,是工程师最常接触的部分,也是除去了网络环境和集群配 置之外对整个Job执行效率影响很大的部分,所以很有必要深入了解整个过程。元旦放假的第一天
    2019-11-10
  • Go打包二进制文件的实现

    Go打包二进制文件的实现

    背景 众所周知,go语言可打包成目标平台二进制文件是其一大优势,如此go项目在服务器不需要配置go环境和依赖就可跑起来。 操作 需求:打包部署到centos7 笔者打包环境:mac os 方法:
    2020-03-11
  • GO语言实现简单的目录复制功能

    GO语言实现简单的目录复制功能

    本文实例讲述了GO语言实现简单的目录复制功能。分享给大家供大家参考。具体实现方法如下: 创建一个独立的 goroutine 遍历文件,主进程负责写入数据。程序会复制空目录,也可以设
    2019-11-10
  • golang中定时器cpu使用率高的现象详析

    golang中定时器cpu使用率高的现象详析

    前言: 废话少说,上线一个用golang写的高频的任务派发系统,上线跑着很稳定,但有个缺点就是当没有任务的时候,cpu的消耗也在几个百分点。 平均值在3%左右的cpu使用率。你没有任务
    2019-11-10