基于寄存器调用惯例的Go语言接口调用机制
本文是对此文章的学习与翻译。
摘要
其实这个机制和Java以及C++的接口调用机制差不多,都是基于itable,不过Java由于虚拟机规范中没有对方法调用惯例做出明确的规定,所以对Java而言,方法调用惯例是基于寄存器还是栈,完全取决于JVM的具体实现,可参考此回答。
Go版本说明
网上的很多博客对于Go语言的方法调用,尤其是接口方法调用的描述已经过时了。因为从1.17版本开始,在AMD64架构上的方法调用就变为了基于寄存器:
Go 1.17 implements a new way of passing function arguments and results using registers instead of the stack. Benchmarks for a representative set of Go packages and programs show performance improvements of about 5%, and a typical reduction in binary size of about 2%. This is currently enabled for Linux, macOS, and Windows on the 64-bit x86 architecture (the
linux/amd64
,darwin/amd64
, andwindows/amd64
ports).
1.18版本更进一步,将基于寄存器的方法调用惯例扩展到了其他平台:
Go 1.17 implemented a new way of passing function arguments and results using registers instead of the stack on 64-bit x86 architecture on selected operating systems. Go 1.18 expands the supported platforms to include 64-bit ARM (
GOARCH=arm64
), big- and little-endian 64-bit PowerPC (GOARCH=ppc64
,ppc64le
), as well as 64-bit x86 architecture (GOARCH=amd64
) on all operating systems. On 64-bit ARM and 64-bit PowerPC systems, benchmarking shows typical performance improvements of 10% or more.
本篇文章基于最新的Go 1.18,架构为AMD64。
样例代码
本篇文章用冒泡排序算法中的内层循环来讲解:
func bubbleUp(x sort.Interface) {
n := x.Len()
for i := 1; i < n; i++ {
if x.Less(i, i-1) {
x.Swap(i, i-1)
}
}
}
这里的sort.Interface是Go标准库里的接口,其定义如下:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
只要实现了sort.Interface接口,就可以当作bubbleUp函数的参数x,但最常用的还是slice。
接口的内存布局
接口在Go runtime中的内部表示如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
在64位机器上,tab和data都是64bit,共128bit,也就是两个4字,其中itab类型定义如下:
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
这里重点关注fun
字段,它是一个可变大小的函数指针数组,这就是分派表
寄存器
目前在AMD64架构上,Go依次使用以下寄存器用作函数调用:
RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11
有时候,一个参数会占用多个寄存器,比如上面说的iface接口,它占用64*2=128bit,那么如果接口值被当作第一个参数传给函数,则其tab字段和data字段将分别占据RAX和RBX寄存器,如果接口值被当作第二个参数传给函数,此时,第一个参数已经占据了RAX,则其tab字段和data字段将分别占据RBX和RCX寄存器,以此类推。顺便提一下,Go语言使用的Plan9汇编语言不使用前缀R来访问这些寄存器(RAX, RBX, RCX, RDI, RSI),而是直接用AX,BX等名字。
汇编代码
使用如下命令将样例代码反汇编:
go tool compile -S bubble.go
关于Go语言的汇编入门,可以参考这篇文章
得到的完整汇编代码见gist。
略去下面这段函数序言:
"".bubbleUp STEXT size=182 args=0x10 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (bubble.go:3) TEXT "".bubbleUp(SB), ABIInternal, $56-16
0x0000 00000 (bubble.go:3) CMPQ SP, 16(R14)
0x0004 00004 (bubble.go:3) PCDATA $0, $-2
0x0004 00004 (bubble.go:3) JLS 152
0x000a 00010 (bubble.go:3) PCDATA $0, $-1
0x000a 00010 (bubble.go:3) SUBQ $56, SP
0x000e 00014 (bubble.go:3) MOVQ BP, 48(SP)
0x0013 00019 (bubble.go:3) LEAQ 48(SP), BP
略去下面这段GC相关代码:
0x0018 00024 (bubble.go:3) FUNCDATA $0, gclocals·09cf9819fc716118c209c2d2155a3632(SB)
0x0018 00024 (bubble.go:3) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0018 00024 (bubble.go:3) FUNCDATA $5, "".bubbleUp.arginfo1(SB)
0x0018 00024 (bubble.go:3) FUNCDATA $6, "".bubbleUp.argliveinfo(SB)
重点来分析函数主体代码:
0x0018 00024 (bubble.go:4) MOVQ AX, "".x+64(SP)
0x001d 00029 (bubble.go:4) MOVQ BX, "".x+72(SP)
0x0022 00034 (bubble.go:4) MOVQ 24(AX), CX
0x0026 00038 (bubble.go:4) MOVQ BX, AX
0x0029 00041 (bubble.go:4) CALL CX
参数暂存
函数开始,如上文所述,接口值将占据AX
,BX
寄存器:其中AX为MOVQ AX, "".x+64(SP)
和MOVQ BX, "".x+72(SP)
将接口值复制到栈上(SP:Stack Pointer),以便为后续AX
,BX
寄存器的使用腾出空间。
找到函数调用地址
接下来,准备调用x.Len()
,此时,就需要找到要调用的函数,而这个函数的入口地址就存放在24(AX)
,其原理是这样的:
- 如前所述,传递给函数bubbleUp的参数
x
,其值存放在AX
,BX
寄存器。 - 根据
iface
的内存布局,AX
寄存器存放的正是指向itab的指针 24(AX)
意思是AX+24
,这里24是十进制,表示指针地址+偏移量24bytesitab
中24bytes偏移量正是指向itab
中的fun
字段,也就是接口中的第一个函数的地址被放置在寄存器CX
中- 而
sort.Interface
接口的第一个方法正是Len()
方法,所以最后,x
的Len()
方法的地址就存放在CX
寄存器中了
调用函数
函数地址找到了,接下来就是调用函数了。因为Len()
方法没有参数,所以唯一一个需要传递的参数就是函数的接收者(receiver),注意到在之前的操作中已经把接口的tab
字段和data
字段将分别存放到了AX
和BX
寄存器,data
字段正是存放了实际的接口值,也就是函数的接收者,所以接下来使用MOVQ BX, AX
将函数的接收者放在AX
寄存器中,然后用CALL CX
调用函数。
函数返回值
Len()
方法返回一个整数n
,存放在AX
寄存器中,这里将返回值存放到了栈上,因为接下来AX
寄存器将用作其他用途:
0x002b 00043 (bubble.go:4) MOVQ AX, "".n+24(SP)
开始循环
接下来的代码就是for循环的汇编实现:
0x0030 00048 (bubble.go:4) MOVL $1, CX
0x0035 00053 (bubble.go:5) JMP 69 --->\
0x0037 00055 (bubble.go:5) MOVQ "".i+32(SP), DX |
0x003c 00060 (bubble.go:5) LEAQ 1(DX), CX |i++
0x0040 00064 (bubble.go:5) MOVQ "".n+24(SP), AX |
0x0045 00069 (bubble.go:5) CMPQ AX, CX <---/
0x0048 00072 (bubble.go:5) JLE 142
0x004a 00074 (bubble.go:5) MOVQ CX, "".i+32(SP)
0x004f 00079 (bubble.go:6) MOVQ "".x+64(SP), DX
0x0054 00084 (bubble.go:6) MOVQ 32(DX), SI
0x0058 00088 (bubble.go:6) LEAQ -1(CX), DI
0x005c 00092 (bubble.go:6) MOVQ DI, ""..autotmp_4+40(SP)
0x0061 00097 (bubble.go:6) MOVQ "".x+72(SP), AX
0x0066 00102 (bubble.go:6) MOVQ CX, BX
0x0069 00105 (bubble.go:6) MOVQ DI, CX
0x006c 00108 (bubble.go:6) CALL SI
0x006e 00110 (bubble.go:6) TESTB AL, AL
0x0070 00112 (bubble.go:6) JEQ 55
0x0072 00114 (bubble.go:7) MOVQ "".x+64(SP), DX
0x0077 00119 (bubble.go:7) MOVQ 40(DX), SI
0x007b 00123 (bubble.go:7) MOVQ "".x+72(SP), AX
0x0080 00128 (bubble.go:7) MOVQ "".i+32(SP), BX
0x0085 00133 (bubble.go:7) MOVQ ""..autotmp_4+40(SP), CX
0x008a 00138 (bubble.go:7) CALL SI
0x008c 00140 (bubble.go:7) JMP 55
0x008e 00142 (bubble.go:10) PCDATA $1, $-1
0x008e 00142 (bubble.go:10) MOVQ 48(SP), BP
i++
的实现:首先将CX
寄存器赋初始值1,然后跳转到第69行CMPQ AX, CX
(注意此时AX
中的值为n
),也就是比较n
和i
的大小,并且当i>=n
时将跳出循环(JLE 142
)。第55行至64行就是i++
的实现。
调用x.Less()
方法:回想一下,itab
中的第一个方法Len()
方法的偏移量为24bytes,所以第二个方法,Less()
方法的偏移量是32bytes。这里首先将存放于栈上的iface
值复制到DX
寄存器中,然后再将Less()
方法的地址放入SI
寄存器,到第105行为止,都是在进行调用x.Less()
方法的准备工作。执行到第105行时,此时:
Less()
方法的接收者x
在AX
寄存器中i
在BX
寄存器中i
-1在CX
寄存器中
Less()
方法返回值存放在AL
寄存器中,0代表false,1代表true,这两行代码判断Less()
方法返回值,若为false,则直接跳到55行,继续下个循环,否则继续执行Swap()
方法:
0x0072 00114 (bubble.go:7) MOVQ "".x+64(SP), DX
0x0077 00119 (bubble.go:7) MOVQ 40(DX), SI
0x007b 00123 (bubble.go:7) MOVQ "".x+72(SP), AX
0x0080 00128 (bubble.go:7) MOVQ "".i+32(SP), BX
0x0085 00133 (bubble.go:7) MOVQ ""..autotmp_4+40(SP), CX
0x008a 00138 (bubble.go:7) CALL SI
紧跟着就是for循环的回边语句:
0x008c 00140 (bubble.go:15) JMP 55
最后就是函数的尾言和返回语句:
0x0093 00147 (bubble.go:10) ADDQ $56, SP
0x0097 00151 (bubble.go:10) RET
0x0098 00152 (bubble.go:10) N