2
头图

Original link: Interviewer: Let's talk about function calling conventions in Go language

foreword

Hello, everyone, my name is asong . It has not been updated for a long time. Recently, I have been busy writing python and Go languages due to work needs, but I can't give up the Go language. I still have to learn it. Today, I will talk to you about the function calling convention of the Go language. The convention is the convention between the caller and the callee for passing parameters and return values. The calling convention of the Go language has been optimized in version 1.17. In this article, let's take a look at the calling conventions of the two versions.

Stack transfer before version 1.17

Before the Go1.17 version, the Go language function calls were passed through the stack. Let's use the Go1.12 version to write an example to see:

package main

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

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

Execute go tool compile -S -N -l main.go to see its assembly instructions. Let's look at it in two parts, first look at the function part of 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

Through the above assembly instructions, we can analyze that the parameters 10 and 20 are pushed from right to left, so the first parameter is at SP~SP+8 at the top of the stack, and the second parameter is stored at SP+8 ~ SP+16 . TEST function, the corresponding assembly instruction: CALL "".Test(SB) , the corresponding assembly instruction is as follows:

"".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 // 函数返回

Through the above assembly instructions, we can draw the conclusion: Go language uses stack to pass parameters and receive return values, and multiple return values are also accomplished by allocating more memory.

This design based on stack passing parameters and receiving return values greatly reduces the complexity of the implementation, but sacrifices the performance of function calls. For example, C language uses both stack and register to pass parameters, which is better than Go language in performance. Let's take a look at the register parameters introduced by Go1.17 .

Why register parameter transfer performance is better than stack parameter transfer

We all know that CPU is the computing core and control core of a computer. Its main function is to interpret computer instructions and process data in computer software. The approximate internal structure of CPU is as follows:

图片来自于网络

It is mainly composed of an arithmetic unit and a controller. The arithmetic unit is responsible for completing arithmetic operations and logical operations. The registers temporarily store the data to be processed by the arithmetic unit and the processed results. Back to the topic, the registers are the internal components of CPU , and are generally stored in Externally, the speed difference between CPU operating registers and reading memory is an order of magnitude. When data calculation is to be performed, if the data is in memory, CPU needs to copy the data from the memory to the register for calculation. Therefore, for stack passing parameters and receiving The calling convention of return value, each calculation needs to be copied from memory to register, and copied back to memory after calculation. If register is used to pass parameters, the parameters are already placed in specific registers in order, which reduces memory and registers Data is copied between, thereby improving performance and providing program operating efficiency.

Since the performance of register passing parameters is higher than that of stack passing parameters, why don't all languages use registers to pass parameters? Because the register differences on different architectures are different, the compiler must support the register parameter transfer, which makes the compiler more complicated and difficult to maintain, and the number of registers is also limited, and it is necessary to consider more than How the parameter for the number of registers should be passed.

1.17 Register based pass

Go language has designed a set of calling conventions based on register parameter transfer in the 1.17 version. Currently, it only supports x86 platform. Let's take a look at it through a simple example:

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

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

Execute go tool compile -S -N -l main.go to see its assembly instructions. Let's look at it in two parts, first look at the function part of 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 // 返回

Through the above assembly instructions, we can analyze that the parameters are no longer pushed from right to left on the stack, and the parameters are directly on the register. After the parameters are prepared, call the TEST function. The corresponding assembly instruction: CALL "".Test(SB) , the corresponding assembly The instructions are as follows:

"".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

Both parameter passing and return use registers to pass, and the return value and input use the exact same register sequence, and the order of use is also the same.

Because of this optimization, in some scenarios where function calls are deeply nested, there is a certain probability that the memory will be reduced. If you have the opportunity to do stress testing, you can try it out~.

Summarize

Mastering and understanding the calling process of functions is an important lesson for us to deeply study the Go language. After reading this article, I hope you have mastered the calling conventions of functions~.

Okay, this article ends here, my name is asong , see you in the next issue.

Welcome to the public account: Golang DreamWorks


asong
605 声望906 粉丝