服务粉丝
我们一直在努力
当前位置: > 科技 >

你们说,不知道Go函数的调用惯例,影响我调函数吗

日期: 来源:用Java可可收集编辑:用Java可可

前言

今天与大家聊一聊 Go 语言的函数调用惯例,调用惯例是调用方和被调用方对于参数和返回值传递的约定,Go语言的调用惯例在1.17版本进行了优化,本文我们就看一下两个版本的调用惯例是什么样的吧~。

1.17版本前栈传递

Go1.17 版本之前, Go 语言函数调用是通过系统来传递的,我们使用 Go1.12 版本写个例子来看一下:

package main

func Test(a, b int) (int, int) {
 return a + b, a - b
}

func main() {
 Test(10, 20)
}

执行 go tool compile -S -N -l main.go 可以看到其汇编指令,我们分两部分来看,先看 main 函数部分:

"".main STEXT size=68 args=0x0 locals=0x28
        0x0000 00000 (main.go:7)        TEXT    "".main(SB), ABIInternal, $40-0
        0x0000 00000 (main.go:7)        MOVQ    (TLS), CX
        0x0009 00009 (main.go:7)        CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:7)        JLS     61
        0x000f 00015 (main.go:7)        SUBQ    $40, SP // 分配40字节栈空间
        0x0013 00019 (main.go:7)        MOVQ    BP, 32(SP) // 基址指针存储到栈上
        0x0018 00024 (main.go:7)        LEAQ    32(SP), BP
        0x001d 00029 (main.go:7)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:7)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:7)        FUNCDATA        $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:8)        PCDATA  $2, $0
        0x001d 00029 (main.go:8)        PCDATA  $0, $0
        0x001d 00029 (main.go:8)        MOVQ    $10, (SP) // 第一个参数压栈
        0x0025 00037 (main.go:8)        MOVQ    $20, 8(SP) // 第二个参数压栈
        0x002e 00046 (main.go:8)        CALL    "".Test(SB) // 调用函数Test 
        0x0033 00051 (main.go:9)        MOVQ    32(SP), BP // Test函数返回后恢复栈基址指针
        0x0038 00056 (main.go:9)        ADDQ    $40, SP // 销毁40字节栈内存
        0x003c 00060 (main.go:9)        RET // 返回
        0x003d 00061 (main.go:9)        NOP
        0x003d 00061 (main.go:7)        PCDATA  $0, $-1
        0x003d 00061 (main.go:7)        PCDATA  $2, $-1
        0x003d 00061 (main.go:7)        CALL    runtime.morestack_noctxt(SB)
        0x0042 00066 (main.go:7)        JMP     0
        0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2e 48  eH..%....H;a.v.H
        0x0010 83 ec 28 48 89 6c 24 20 48 8d 6c 24 20 48 c7 04  ..(H.l$ H.l$ H..
        0x0020 24 0a 00 00 00 48 c7 44 24 08 14 00 00 00 e8 00  $....H.D$.......
        0x0030 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 e8 00 00  ...H.l$ H..(....
        0x0040 00 00 eb bc                                      ....
        rel 5+4 t=16 TLS+0
        rel 47+4 t=8 "".Test+0
        rel 62+4 t=8 runtime.morestack_noctxt+0

通过上面的汇编指令我们可以分析出,参数 1020 按照从右向左进行压栈,所以第一个参数在栈顶的位置 SP~SP+8 ,第二个参数存储在 SP+8 ~ SP+16 ,参数准备完毕后就去调用 TEST 函数,对应的汇编指令: CALL "".Test(SB) ,对应的汇编指令如下:

"".Test STEXT nosplit size=49 args=0x20 locals=0x0
        0x0000 00000 (main.go:3)        TEXT    "".Test(SB), NOSPLIT|ABIInternal, $0-32
        0x0000 00000 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:3)        FUNCDATA        $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (main.go:3)        PCDATA  $2, $0
        0x0000 00000 (main.go:3)        PCDATA  $0, $0
        0x0000 00000 (main.go:3)        MOVQ    $0, "".~r2+24(SP)// SP+16 ~ SP+24 存储第一个返回值
        0x0009 00009 (main.go:3)        MOVQ    $0, "".~r3+32(SP)
// SP+24 ~ SP+32 存储第二个返回值
        0x0012 00018 (main.go:4)        MOVQ    "".a+8(SP), AX // 第一个参数放入AX寄存器 AX = 10
        0x0017 00023 (main.go:4)        ADDQ    "".b+16(SP), AX // 第二个参数放入AX寄存器做加法 AX = AX + 20 = 30
        0x001c 00028 (main.go:4)        MOVQ    AX, "".~r2+24(SP)
// AX寄存器中的值在存回栈中:24(SP)
        0x0021 00033 (main.go:4)        MOVQ    "".a+8(SP), AX
// 第一个参数放入AX寄存器 AX = 10
        0x0026 00038 (main.go:4)        SUBQ    "".b+16(SP), AX
// 第二个参数放入AX寄存器做减法 AX = AX - 20 = -10
        0x002b 00043 (main.go:4)        MOVQ    AX, "".~r3+32(SP)
// AX寄存器中的值在存回栈中:32(SP)
        0x0030 00048 (main.go:4)        RET // 函数返回

通过以上的汇编指令我们可以得出结论: Go 语言使用栈传递参数和接收返回值,多个返回值也是通过多分配一些内存来完成的。

这种基于函数传递参数和接收返回值的设计大大降低了实现的复杂度,但是牺牲了函数调用的性能,像 C 语言采用同时使用栈和寄存器传递参数,在性能上是优于 Go 语言的,下面我们就来看一看 Go1.17 引入的寄存器传参。

为什么寄存器传参性能优于栈传参

我们都知道 CPU 是一台计算机的运算核心和控制核心,其主要功能是解释计算机指令以及处理计算机软件中的数据, CPU 大致内部结构如下:

图片来自于网络

主要由运算器和控制器组成,运算器负责完成算术运算和逻辑运算,寄存器临时保存将要被运算器处理的数据和处理后的结果,回到主题上,寄存器是 CPU 内部组件,存储一般在外部, CPU 操作寄存器与读取内存的速度差距是数量级别的,当要进行数据计算时,如果数据处于内存中, CPU 需要先将数据从内存拷贝到寄存器进行计算,所以对于栈传递参数与接收返回值这种调用规约,每次计算都需要从内存拷贝到寄存器,计算完毕在拷贝回内存,如果使用寄存器传参的话,参数就已经按顺序放在特定寄存器了,这样就减少了内存和寄存器之间的数据拷贝,从而改善了性能,提供程序运行效率。

既然寄存器传参性能高于栈传递参数,为什么所有语言不都使用寄存器传递参数呢?因为不同架构上的寄存器差异不同,所以要支持寄存器传参就要在编译器上进行支持,这要就使编译器变得更加复杂且不易维护,并且寄存器的数量也是有限的,还要考虑超过寄存器数量的参数应该如何传递。

1.17基于寄存器传递

Go 语言在 1.17 版本设计了一套基于寄存器传参的调用规约,目前也只支持 x86 平台,我们也是通过一个简单的例子来看一下:

func Test(a, b, c, d int) (int,int,int,int) {
 return a, b, c, d
}

func main()  {
 Test(1, 2, 3 ,4)
}

执行 go tool compile -S -N -l main.go 可以看到其汇编指令,我们分两部分来看,先看 main 函数部分:

"".main STEXT size=62 args=0x0 locals=0x28 funcid=0x0
        0x0000 00000 (main.go:7)        TEXT    "".main(SB), ABIInternal, $40-0
        0x0000 00000 (main.go:7)        CMPQ    SP, 16(R14)
        0x0004 00004 (main.go:7)        PCDATA  $0, $-2
        0x0004 00004 (main.go:7)        JLS     55
        0x0006 00006 (main.go:7)        PCDATA  $0, $-1
        0x0006 00006 (main.go:7)        SUBQ    $40, SP// 分配40字节栈空间,基址指针存储到栈上
        0x000a 00010 (main.go:7)        MOVQ    BP, 32(SP)// 基址指针存储到栈上
        0x000f 00015 (main.go:7)        LEAQ    32(SP), BP
        0x0014 00020 (main.go:7)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:7)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0014 00020 (main.go:8)        MOVL    $1, AX // 参数1使用AX寄存器传递
        0x0019 00025 (main.go:8)        MOVL    $2, BX // 参数2使用BX寄存器传递
        0x001e 00030 (main.go:8)        MOVL    $3, CX // 参数3使用CX寄存器传递
        0x0023 00035 (main.go:8)        MOVL    $4, DI // 参数4使用DI寄存器传递
        0x0028 00040 (main.go:8)        PCDATA  $1, $0
        0x0028 00040 (main.go:8)        CALL    "".Test(SB) // 调用Test函数
        0x002d 00045 (main.go:9)        MOVQ    32(SP), BP // Test函数返回后恢复栈基址指针
        0x0032 00050 (main.go:9)        ADDQ    $40, SP // 销毁40字节栈内存
        0x0036 00054 (main.go:9)        RET // 返回

通过上面的汇编指令我们可以分析出,现在参数已经不是从右向左进行压缩了,参数直接在寄存器上了,参数准备完毕后就去调用 TEST 函数,对应的汇编指令: CALL "".Test(SB) ,对应的汇编指令如下:

"".Test STEXT nosplit size=133 args=0x20 locals=0x28 funcid=0x0
        0x0000 00000 (main.go:3)        TEXT    "".Test(SB), NOSPLIT|ABIInternal, $40-32
        0x0000 00000 (main.go:3)        SUBQ    $40, SP
        0x0004 00004 (main.go:3)        MOVQ    BP, 32(SP)
        0x0009 00009 (main.go:3)        LEAQ    32(SP), BP
        0x000e 00014 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (main.go:3)        FUNCDATA        $5, "".Test.arginfo1(SB)
0x000e 00014 (main.go:3)        MOVQ    AX, "".a+48(SP) // 从寄存器AX获取参数 1 放入栈中 48(SP)
0x0013 00019 (main.go:3)        MOVQ    BX, "".b+56(SP) // 从寄存器BX获取参数 2 放入栈中 56(SP)
0x0018 00024 (main.go:3)        MOVQ    CX, "".c+64(SP) // 从寄存器CX获取参数 3 放入栈中 64(SP)
0x001d 00029 (main.go:3)        MOVQ    DI, "".d+72(SP) // 从寄存器DI获取参数 4 放入栈中 72(SP)
        0x0022 00034 (main.go:3)        MOVQ    $0, "".~r4+24(SP)
        0x002b 00043 (main.go:3)        MOVQ    $0, "".~r5+16(SP)
        0x0034 00052 (main.go:3)        MOVQ    $0, "".~r6+8(SP)
        0x003d 00061 (main.go:3)        MOVQ    $0, "".~r7(SP)
        0x0045 00069 (main.go:4)        MOVQ    "".a+48(SP), DX // 以下操作是返回值放到寄存器中返回
        0x004a 00074 (main.go:4)        MOVQ    DX, "".~r4+24(SP)
        0x004f 00079 (main.go:4)        MOVQ    "".b+56(SP), DX
        0x0054 00084 (main.go:4)        MOVQ    DX, "".~r5+16(SP)
        0x0059 00089 (main.go:4)        MOVQ    "".c+64(SP), DX
        0x005e 00094 (main.go:4)        MOVQ    DX, "".~r6+8(SP)
        0x0063 00099 (main.go:4)        MOVQ    "".d+72(SP), DI
        0x0068 00104 (main.go:4)        MOVQ    DI, "".~r7(SP)
        0x006c 00108 (main.go:4)        MOVQ    "".~r4+24(SP), AX
        0x0071 00113 (main.go:4)        MOVQ    "".~r5+16(SP), BX
        0x0076 00118 (main.go:4)        MOVQ    "".~r6+8(SP), CX
        0x007b 00123 (main.go:4)        MOVQ    32(SP), BP
        0x0080 00128 (main.go:4)        ADDQ    $40, SP
        0x0084 00132 (main.go:4)        RET

传参和返回都采用了寄存器进行传递,并且返回值和输入都使用了完全相同的寄存器序列,并且使用的顺序也是一致的。

因为这个优化,在一些函数调用嵌套层次较深的场景下,内存有一定概率会降低,有机会做压测可以试一试~。


总结

熟练掌握并理解函数的调用过程是我们深入学习 Go 语言的重要一课,看完本文希望你已经熟练掌握了函数的调用惯例~。

相关阅读

  • 正态分布公式为什么长这样?

  • 正态分布是最常见的连续概率分布,在日常生活和科学研究中都起到重要作用。正态分布的密度函数具有数学美感,但很多人并不知道它是如何得到的,本文介绍两种推导方法。撰文 | Lon
  • 利用几何画板制作二次函数图象的动态演示

  • 首先来看一下效果图,以下是二次函数一般式参数变化时的图象变化情况a单独变化时的效果图b单独变化时的效果图c单独变化时的效果图a、b、c一起变化时的效果图感兴趣的读者可以
  • Python编程中输出结果保留两位小数的技巧

  • 在Python中,我们经常需要对浮点数进行四舍五入,使其保留特定数量的小数位。这不仅有助于保持数据的准确性,还能让输出结果更加整洁。#百享新春分亿金#本文将介绍多种保留两位小
  • 分段函数之旅:Python编程实战

  • 分段函数是一类在数学和编程中常见的函数类型,它们由多个不同的函数段组成,每个函数段只在特定的输入范围内有效。、Python作为一种强大而灵活的编程语言,可以轻松实现各种分段
  • 不要老是想着寻找自我 多多重视常规和惯例

  • 编者按:一个人不能通过追求自我来“找到自我”,相反,应该通过追求别的东西,通过学习一些纪律或常规惯例……才能搞清你是谁,你想成为谁。本文来自编译。我曾与一位男子共事,他是一
  • 解密数据之道:探索Python中zip的神奇应用

  • zip函数在Python中扮演着重要角色。通过学习zip函数的用法,我们可以快速、高效地处理多个序列的数据,实现数据的压缩、合并和解密等功能。下面,让我们一起揭开Python中zip函数
  • 创造互动:揭秘Python中的输入函数

  • 输入函数input在Python中扮演着与用户互动的重要角色通过学习输入函数的用法,我们能够实现灵活的用户输入、数据转换和错误处理,为我们的程序增添互动的乐趣。让我们一起来揭
  • 忽略隐藏列求和汇总,CELL函数的强项!

  • 1职场实例小伙伴们大家好,今天我们来讲解一个Excel表格职场实例:如何忽略隐藏列求和汇总。这个问题好像听起来很简单的样子,但是实际操作起来却需要一定的知识储备。如下图所示

热门文章

  • 解码“新IT”的5个特征和3大价值

  • 党的二十大报告提出,要加快发展数字经济,促进数字经济和实体经济深度融合,打造具有国际竞争力的数字产业集群。在数字经济与实体经济深度融合的产业浪潮中,以智能设备、边缘计算
  • OPPO k1的低价高配真实么?网友:不看不知道

  • 近日OPPO一款新机OPPO k1,摒弃了高价低配,就连自家老大哥r17都要怼一下。更是放弃了请代言人,以往的OPPO手机还没出来,各路流量小生,花样美男的代言就先来了。还有线下销售人员的
  • 一招教你手机无限制成为一台新设备

  • 大家平时用手机去注册app,肯定会遇到检测设备异常,交易关闭,等问题 这个都是手机已经不止1-2次注册过此app,不断更换手机仅是一个暂时的方法,却不是长久之计,手机总归会用完
  • 骁龙845加持美图V7硬件升级 麒麟980拍照怎么样?

  • 现阶段,市面上搭载骁龙845和麒麟980的智能手机有不少,这些机型的拍照的能力也是购机重要的选择因素。骁龙845和麒麟980这样的手机芯片对于拍摄效果关系密切,用户对手机的拍照要

最新文章

  • 野外驻训丨多措并举走好野外驻训第一步

  • 7月9日,甘孜州森林消防支队在康定市新都桥镇隆重举行了“甘孜州森林消防支队2024年度野外驻训暨‘火焰蓝’比武开幕动员”,同日,康定市森林消防中队全体指战员顺利进驻野外驻训