简单看看 Go 1.17 的新版调用规约
Go 1.17 修改了用了很久的基于棧的調用規約,在了解 Go 的調用規約之前,我們得知道什么是調用規約。
x86 calling convention[1],簡單概括一下,其實就是語言對于函數之間傳參的一種約定。調用方要知道我要把參數按照什么形式,什么順序傳給被調用函數,被調用函數也遵守該規范去相應的位置找到傳入的參數內容。
老版本的 Go 的參數傳遞圖我們已經在很多很多地方見過了,這里貼一個我之前畫的:
可以看到入參和返回值都在棧上,按順序,從低地址,到高地址排列。
這種基于棧的傳參在設計和實現上確實要簡單,但棧上傳參會導致函數調用過程中數次發生從寄存器和內存之間的參數搬運操作。比如 call 的時候,要把參數全搬到 SP 的位置(這里從寄存器 -> 內存);ret 的時候,也要把參數從寄存器搬到 FP 位置。ret 完畢之后,要把返回值從內存 -> 寄存器。
寄存器是 CPU 內部的組件,而主存一般都在外部,兩者之間有數量級的性能差異,所以一直有人說 Go 的函數調用性能很差,需要優化(雖然這些人大概率也不是從系統整體性能考慮去做優化的)。
Go 1.17 設計了一套基于寄存器傳參的調用規約,目前只在 x86 平臺下開啟,我們可以通過反匯編對其進行簡單的觀察。這里依然為了簡化問題,我們只用 int 參數(float 使用的不是通用寄存器,其它數據結構需要展開傳參,也不難,就是稍微麻煩一點)。
package?main//go:noinline func?add(x?int,?y?int,?z?int,?a,?b,?c?int,?d,?e,?f?int,?g,?h,?l?int)?(int,?int,?int,?int,?int,?int,?int,?int,?int,?int,?int)?{ println(x,?y) return?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11 }func?main()?{ println(add(1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)) }稍微多傳一些參數方便我們觀察,輸入 12 個參數,返回 11 個值。
直接看反匯編的結果,首先是對 main.add 的調用部分:
TEXT?main.main(SB)?/Users/xargin/test/abi.goabi.go:15?????????????0x1054e60???????????????4c8da42478ffffff????????LEAQ?0xffffff78(SP),?R12abi.go:15?????????????0x1054e68???????????????4d3b6610????????????????CMPQ?0x10(R14),?R12abi.go:15?????????????0x1054e6c???????????????0f865a020000????????????JBE?0x10550ccabi.go:15?????????????0x1054e72???????????????4881ec08010000??????????SUBQ?$0x108,?SPabi.go:15?????????????0x1054e79???????????????4889ac2400010000????????MOVQ?BP,?0x100(SP)abi.go:15?????????????0x1054e81???????????????488dac2400010000????????LEAQ?0x100(SP),?BPabi.go:16?????????????0x1054e89???????????????48c704240a000000????????MOVQ?$0xa,?0(SP)?//?第?10?個參數abi.go:16?????????????0x1054e91???????????????48c74424080b000000??????MOVQ?$0xb,?0x8(SP)?//?第?11?個參數abi.go:16?????????????0x1054e9a???????????????48c74424100c000000??????MOVQ?$0xc,?0x10(SP)?//?第?12?個參數abi.go:16?????????????0x1054ea3???????????????b801000000??????????????MOVL?$0x1,?AX?//?第?1?個參數,后面以此類推abi.go:16?????????????0x1054ea8???????????????bb02000000??????????????MOVL?$0x2,?BXabi.go:16?????????????0x1054ead???????????????b903000000??????????????MOVL?$0x3,?CXabi.go:16?????????????0x1054eb2???????????????bf04000000??????????????MOVL?$0x4,?DIabi.go:16?????????????0x1054eb7???????????????be05000000??????????????MOVL?$0x5,?SIabi.go:16?????????????0x1054ebc???????????????41b806000000????????????MOVL?$0x6,?R8abi.go:16?????????????0x1054ec2???????????????41b907000000????????????MOVL?$0x7,?R9abi.go:16?????????????0x1054ec8???????????????41ba08000000????????????MOVL?$0x8,?R10abi.go:16?????????????0x1054ece???????????????41bb09000000????????????MOVL?$0x9,?R11abi.go:16?????????????0x1054ed4???????????????e807fdffff??????????????CALL?main.add(SB)abi.go:16?????????????0x1054ed9???????????????48898424f8000000????????MOVQ?AX,?0xf8(SP)可以看到,官方只使用了 9 個通用寄存器,依次是 AX,BX,CX,DI,SI,R8,R9,R10,R11,超出部分,按順序放在棧上。
然后是 main.add 的返回值部分:
TEXT?main.add(SB)?/Users/xargin/test/abi.go ....??省略?print?的部分abi.go:6??????????????0x1054c2f???????????????48c74424400a000000??????MOVQ?$0xa,?0x40(SP)?//?第?10?個返回值abi.go:6??????????????0x1054c38???????????????48c74424480b000000??????MOVQ?$0xb,?0x48(SP)?//?第?11?個返回值abi.go:6??????????????0x1054c41???????????????b801000000??????????????MOVL?$0x1,?AX?//?第?1?個返回值,后面以此類推abi.go:6??????????????0x1054c46???????????????bb02000000??????????????MOVL?$0x2,?BXabi.go:6??????????????0x1054c4b???????????????b903000000??????????????MOVL?$0x3,?CXabi.go:6??????????????0x1054c50???????????????bf04000000??????????????MOVL?$0x4,?DIabi.go:6??????????????0x1054c55???????????????be05000000??????????????MOVL?$0x5,?SIabi.go:6??????????????0x1054c5a???????????????41b806000000????????????MOVL?$0x6,?R8abi.go:6??????????????0x1054c60???????????????41b907000000????????????MOVL?$0x7,?R9abi.go:6??????????????0x1054c66???????????????41ba08000000????????????MOVL?$0x8,?R10abi.go:6??????????????0x1054c6c???????????????41bb09000000????????????MOVL?$0x9,?R11abi.go:6??????????????0x1054c72???????????????488b6c2418??????????????MOVQ?0x18(SP),?BPabi.go:6??????????????0x1054c77???????????????4883c420????????????????ADDQ?$0x20,?SPabi.go:6??????????????0x1054c7b???????????????c3??????????????????????RET返回值和輸入使用了完全相同的寄存器序列,同樣在超出 9 個返回值時,多出的內容在棧上返回。
在傳統的調用規約中,一般會區分 caller saved registers 和 callee saved registers,但在 Go 中,所有寄存器都是 caller saved,也就是由 caller 負責保存,在 callee 中不保證不對其現場進行破壞。
這里可以看到,返回值直接?把入參使用的寄存器??覆蓋掉了,也可以證明這一點。
因為函數調用不需要通過棧來傳參了,所以在一些函數調用嵌套層次比較深的場景下,goroutine 棧本身使用的內存也有一定概率會降低。不過因為暫時手邊沒有什么生產環境,暫時也無法驗證就是了。
在 Go 語言中,除了基本的函數調用傳參,在 reflect 包中有 reflect.Call 的 API 也可以執行函數調用;此外還有 defer 語句中也可以執行函數調用,在創建 goroutine 時也涉及給函數傳參。
我們同樣可以使用反匯編的手段,來觀察這些場景下是否已經進行了基于寄存器的調用優化,本文就不再贅述了。
[1]
x86 calling convention:?https://en.wikipedia.org/wiki/X86_calling_conventions
總結
以上是生活随笔為你收集整理的简单看看 Go 1.17 的新版调用规约的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搞定系统设计 02:估算的一些方法
- 下一篇: 实战演示 Go 反射的使用方法和应用场景