Go 新的函數(shù)調(diào)用慣例能快多少?
回復(fù)“Go語言”即可獲贈(zèng)從入門到進(jìn)階共10本電子書
編譯器和運(yùn)行時(shí)的不斷優(yōu)化,能讓 Go 程序的構(gòu)建與運(yùn)行更加絲滑。在 Go 1.18 的 release notes 中,菜刀發(fā)現(xiàn) Go 新的函數(shù)調(diào)用慣例(基于寄存器)將擴(kuò)展支持到 arm64 架構(gòu)(已支持 amd64),且性能提升10% 以上,值得期待。
本文一起來看下函數(shù)調(diào)用慣例的改變能給 Go 帶來多少收益。

函數(shù)調(diào)用慣例
在Go函數(shù)調(diào)用慣例一文中(建議不熟悉此塊內(nèi)容的讀者先讀此文),我們探討過 Go 語言的函數(shù)調(diào)用慣例。
所謂函數(shù)調(diào)用慣例指的是函數(shù)調(diào)用方與被調(diào)用方中必須遵守的某種約定,主要包括函數(shù)的出入?yún)鬟f方式、傳遞順序等。
參數(shù)傳遞方式一般分為兩種情況:寄存器傳遞和棧傳遞。
在 Go 1.17 之前,Go 語言為了避免不同 CPU 寄存器之間的差異,采用的棧傳遞。這種方式的最大優(yōu)點(diǎn)是實(shí)現(xiàn)簡單,讓編譯器易于維護(hù)。但缺點(diǎn)也顯而易見:會(huì)犧牲一些性能。因?yàn)?CPU 訪問寄存器的速度會(huì)遠(yuǎn)高于內(nèi)存。
改變
基于性能考慮,寄存器的調(diào)用慣例,是大多數(shù)語言采納的方式。Go 也準(zhǔn)備做點(diǎn)改變,在 1.17 版本中,對于 linux/amd64, darwin/amd64, windows/amd64 系統(tǒng),首先實(shí)現(xiàn)了新的基于寄存器的調(diào)用慣例。
package?main
//go:noinline
func?add(i,?j?int)?int?{
?return?i?+?j
}
func?main()?{
?add(100,?200)
}
我們在 darwin/amd64 系統(tǒng)上,分別使用 Go 1.17 和 Go 1.16 的代碼進(jìn)行編譯,得到它們的匯編語句分別如下。
Go 1.17 匯編語句
$?go?version
go?version?go1.17?darwin/amd64
$?go?tool?compile?-S?main.go
"".add?STEXT?nosplit?size=4?args=0x10?locals=0x0?funcid=0x0
?0x0000?00000?(main.go:4)?TEXT?"".add(SB),?NOSPLIT|ABIInternal,?$0-16
?0x0000?00000?(main.go:4)?FUNCDATA?$0,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0000?00000?(main.go:4)?FUNCDATA?$1,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0000?00000?(main.go:4)?FUNCDATA?$5,?"".add.arginfo1(SB)
?0x0000?00000?(main.go:5)?ADDQ?BX,?AX
?0x0003?00003?(main.go:5)?RET
?0x0000?48?01?d8?c3??????????????????????????????????????H...
"".main?STEXT?size=54?args=0x0?locals=0x18?funcid=0x0
?0x0000?00000?(main.go:8)?TEXT?"".main(SB),?ABIInternal,?$24-0
?0x0000?00000?(main.go:8)?CMPQ?SP,?16(R14)
?0x0004?00004?(main.go:8)?PCDATA?$0,?$-2
?0x0004?00004?(main.go:8)?JLS?47
?0x0006?00006?(main.go:8)?PCDATA?$0,?$-1
?0x0006?00006?(main.go:8)?SUBQ?$24,?SP
?0x000a?00010?(main.go:8)?MOVQ?BP,?16(SP)
?0x000f?00015?(main.go:8)?LEAQ?16(SP),?BP
?0x0014?00020?(main.go:8)?FUNCDATA?$0,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0014?00020?(main.go:8)?FUNCDATA?$1,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0014?00020?(main.go:9)?MOVL?$100,?AX
?0x0019?00025?(main.go:9)?MOVL?$200,?BX
?0x001e?00030?(main.go:9)?PCDATA?$1,?$0
?0x001e?00030?(main.go:9)?NOP
?0x0020?00032?(main.go:9)?CALL?"".add(SB)
?0x0025?00037?(main.go:10)?MOVQ?16(SP),?BP
?0x002a?00042?(main.go:10)?ADDQ?$24,?SP
?0x002e?00046?(main.go:10)?RET
?0x002f?00047?(main.go:10)?NOP
?0x002f?00047?(main.go:8)?PCDATA?$1,?$-1
?0x002f?00047?(main.go:8)?PCDATA?$0,?$-2
?0x002f?00047?(main.go:8)?CALL?runtime.morestack_noctxt(SB)
?0x0034?00052?(main.go:8)?PCDATA?$0,?$-1
?0x0034?00052?(main.go:8)?JMP?0
?...
Go 1.16 匯編語句
$?go1.16.4?version
go?version?go1.16.4?darwin/amd64
$?go1.16.4?tool?compile?-S?main.go
"".add?STEXT?nosplit?size=19?args=0x18?locals=0x0?funcid=0x0
?0x0000?00000?(main.go:4)?TEXT?"".add(SB),?NOSPLIT|ABIInternal,?$0-24
?0x0000?00000?(main.go:4)?FUNCDATA?$0,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0000?00000?(main.go:4)?FUNCDATA?$1,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x0000?00000?(main.go:5)?MOVQ?"".j+16(SP),?AX
?0x0005?00005?(main.go:5)?MOVQ?"".i+8(SP),?CX
?0x000a?00010?(main.go:5)?ADDQ?CX,?AX
?0x000d?00013?(main.go:5)?MOVQ?AX,?"".~r2+24(SP)
?0x0012?00018?(main.go:5)?RET
?0x0000?48?8b?44?24?10?48?8b?4c?24?08?48?01?c8?48?89?44??H.D$.H.L$.H..H.D
?0x0010?24?18?c3?????????????????????????????????????????$..
"".main?STEXT?size=71?args=0x0?locals=0x20?funcid=0x0
?0x0000?00000?(main.go:8)?TEXT?"".main(SB),?ABIInternal,?$32-0
?0x0000?00000?(main.go:8)?MOVQ?(TLS),?CX
?0x0009?00009?(main.go:8)?CMPQ?SP,?16(CX)
?0x000d?00013?(main.go:8)?PCDATA?$0,?$-2
?0x000d?00013?(main.go:8)?JLS?64
?0x000f?00015?(main.go:8)?PCDATA?$0,?$-1
?0x000f?00015?(main.go:8)?SUBQ?$32,?SP
?0x0013?00019?(main.go:8)?MOVQ?BP,?24(SP)
?0x0018?00024?(main.go:8)?LEAQ?24(SP),?BP
?0x001d?00029?(main.go:8)?FUNCDATA?$0,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x001d?00029?(main.go:8)?FUNCDATA?$1,?gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
?0x001d?00029?(main.go:9)?MOVQ?$100,?(SP)
?0x0025?00037?(main.go:9)?MOVQ?$200,?8(SP)
?0x002e?00046?(main.go:9)?PCDATA?$1,?$0
?0x002e?00046?(main.go:9)?CALL?"".add(SB)
?0x0033?00051?(main.go:10)?MOVQ?24(SP),?BP
?0x0038?00056?(main.go:10)?ADDQ?$32,?SP
?0x003c?00060?(main.go:10)?RET
?0x003d?00061?(main.go:10)?NOP
?0x003d?00061?(main.go:8)?PCDATA?$1,?$-1
?0x003d?00061?(main.go:8)?PCDATA?$0,?$-2
?0x003d?00061?(main.go:8)?NOP
?0x0040?00064?(main.go:8)?CALL?runtime.morestack_noctxt(SB)
?0x0045?00069?(main.go:8)?PCDATA?$0,?$-1
?0x0045?00069?(main.go:8)?JMP?0
看到這么多匯編代碼,不要緊張。這里我們需要留意的就以下這么幾行
//?Go?1.17?匯編參數(shù)調(diào)用代碼
"".add?STEXT?nosplit?size=4?args=0x10?locals=0x0?funcid=0x0
...
0x0000?00000?(main.go:5)?ADDQ?BX,?AX
...
"".main?STEXT?size=54?args=0x0?locals=0x18?funcid=0x0
...
?0x0014?00020?(main.go:9)?MOVL?$100,?AX
?0x0019?00025?(main.go:9)?MOVL?$200,?BX
?0x001e?00030?(main.go:9)?PCDATA?$1,?$0
?0x001e?00030?(main.go:9)?NOP
?0x0020?00032?(main.go:9)?CALL?"".add(SB)
...
//?Go?1.16?匯編參數(shù)調(diào)用代碼
"".add?STEXT?nosplit?size=19?args=0x18?locals=0x0?funcid=0x0
...
?0x0000?00000?(main.go:5)?MOVQ?"".j+16(SP),?AX
?0x0005?00005?(main.go:5)?MOVQ?"".i+8(SP),?CX
?0x000a?00010?(main.go:5)?ADDQ?CX,?AX
?0x000d?00013?(main.go:5)?MOVQ?AX,?"".~r2+24(SP)
...
"".main?STEXT?size=71?args=0x0?locals=0x20?funcid=0x0
...
?0x001d?00029?(main.go:9)?MOVQ?$100,?(SP)
?0x0025?00037?(main.go:9)?MOVQ?$200,?8(SP)
?0x002e?00046?(main.go:9)?PCDATA?$1,?$0
?0x002e?00046?(main.go:9)?CALL?"".add(SB)
...
看出差異了嗎?
在 Go 1.17 的匯編代碼中,參數(shù)值 100 和 200 直接基于寄存器 AX 和 BX 來操作。而 Go 1.16 中,參數(shù)值是通過指向棧頂?shù)臈V羔樇拇嫫鱏P的偏移量來表示和傳遞的。
在 Go 1.17 的release notes中,編譯器的此項(xiàng)改變會(huì)讓 Go 程序運(yùn)行性能和二進(jìn)制大小兩個(gè)方面得到優(yōu)化,
二進(jìn)制大小
首先,我們比較編譯后的二進(jìn)制大小。
$?go?build?-o?main1.17?main.go
$?go1.16.4?build?-o?main1.16?main.go
$?ls?-al?main1.17?main1.16
-rwxr-xr-x??1?slp??staff??1200640?Dec?26?21:09?main1.16
-rwxr-xr-x??1?slp??staff??1142208?Dec?26?21:09?main1.17
可以看出,Go 1.17 基于寄存器傳遞的函數(shù)調(diào)用慣例編譯出的二進(jìn)制,相較于 Go 1.16 基于棧傳遞的減少 4.8% 的大小。
性能
通過 benchmark 比較程序執(zhí)行效率
//?Go?1.17
$?go?test?-bench=.
goos:?darwin
goarch:?amd64
pkg:?workspace/add
cpu:?Intel(R)?Core(TM)?i5-8279U?CPU?@?2.40GHz
BenchmarkIt-8????918887481??????????1.257?ns/op
PASS
ok???workspace/add?1.299s
//?Go?1.16
$?go1.16.4?test?-bench=.
goos:?darwin
goarch:?amd64
pkg:?workspace/add
cpu:?Intel(R)?Core(TM)?i5-8279U?CPU?@?2.40GHz
BenchmarkIt-8????801041754??????????1.469?ns/op
PASS
ok???workspace/add?1.336s
從 1.469 ns/op 提升至 1.257 ns/op,大約提升了 14%。
總結(jié)
我們常談?wù)摰剑珿o 是在不斷優(yōu)化迭代的,我們值得期待與建設(shè)更好的 Go 語言。
為了降低基于棧傳遞的性能損耗,從 Go 1.17 起,引入了基于寄存器傳遞的編譯改變,目前只支持 amd64 平臺(tái)。但在 Go 1.18 中,將擴(kuò)展對 arm64、ppc64、ppc64le 平臺(tái)的支持。
如 Go 的 release notes 所述,新的函數(shù)調(diào)用慣例將會(huì)帶來兩個(gè)方面的提升:編譯后的二進(jìn)制大小會(huì)更小,執(zhí)行效率得到提升。同時(shí),為了保持與現(xiàn)有匯編函數(shù)的兼容性,編譯器生成了在新舊調(diào)用約定之間進(jìn)行轉(zhuǎn)換的適配器函數(shù)。
-------------------?End?-------------------
往期精彩文章推薦:

歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Go學(xué)習(xí)群請?jiān)诤笈_(tái)回復(fù)【入群】
萬水千山總是情,點(diǎn)個(gè)【在看】行不行
