前言
我們知道 Go 語(yǔ)言的三位領(lǐng)導(dǎo)者中有兩位來(lái)自 Plan 9 項(xiàng)目,這直接導(dǎo)致了 Go 語(yǔ)言的匯編采用了比較有個(gè)性的 Plan 9 風(fēng)格。不過(guò),我們不能因咽廢食而放棄無(wú)所不能的匯編。
1、 Go 匯編基礎(chǔ)知識(shí)
1.1、通用寄存器
不同體系結(jié)構(gòu)的 CPU,其內(nèi)部寄存器的數(shù)量、種類(lèi)以及名稱(chēng)可能大不相同,這里我們只介紹 AMD64 的寄存器。AMD64 有 20 多個(gè)可以直接在匯編代碼中使用的寄存器,其中有幾個(gè)寄存器在操作系統(tǒng)代碼中才會(huì)見(jiàn)到,而應(yīng)用層代碼一般只會(huì)用到如下三類(lèi)寄存器。

AMD64 的通用通用寄存器的名字在 plan9 中的對(duì)應(yīng)關(guān)系:
| AMD64 | RAX | RBX | RCX | RDX | RDI | RSI | RBP | RSP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | RIP |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Plan9 | AX | BX | CX | DX | DI | SI | BP | SP | R8 | R9 | R10 | R11 | R12 | R13 | R14 | PC |
Go 語(yǔ)言中寄存器一般用途:

1.2、偽寄存器
偽寄存器是 plan9 偽匯編中的一個(gè)助記符, 也是 Plan9 比較有個(gè)性的語(yǔ)法之一。常見(jiàn)偽寄存器如下表所示:

SB:指向全局符號(hào)表。相對(duì)于寄存器,SB 更像是一個(gè)聲明標(biāo)識(shí),用于標(biāo)識(shí)全局變量、函數(shù)等。通過(guò) symbol(SB) 方式使用,symbol<>(SB)表示 symbol 只在當(dāng)前文件可見(jiàn),跟 C 中的 static 效果類(lèi)似。此外可以在引用上加偏移量,如 symbol+4(SB) 表示 symbol+4bytes 的地址。
PC:程序計(jì)數(shù)器(Program Counter),指向下一條要執(zhí)行的指令的地址,在 AMD64 對(duì)應(yīng) rip 寄存器。個(gè)人覺(jué)得,把他歸為偽寄存器有點(diǎn)令人費(fèi)解,可能是因?yàn)槊總€(gè)平臺(tái)對(duì)應(yīng)的物理寄存器名字不一樣。
SP:SP 寄存器比較特殊,既可以當(dāng)做物理寄存器也可以當(dāng)做偽寄存器使用,不過(guò)這兩種用法的使用語(yǔ)法不同。其中,偽寄存器使用語(yǔ)法是 symbol+offset(SP),此場(chǎng)景下 SP 指向局部變量的起始位置(高地址處);x-8(SP) 表示函數(shù)的第一個(gè)本地變量;物理 SP(硬件SP) 的使用語(yǔ)法則是 +offset(SP),此場(chǎng)景下 SP 指向真實(shí)棧頂?shù)刂罚畹偷刂诽帲?/p>
FP:用于標(biāo)識(shí)函數(shù)參數(shù)、返回值。被調(diào)用者(callee)的 FP 實(shí)際上是調(diào)用者(caller)的棧頂,即 callee.SP(物理SP) == caller.FP;x+0(FP) 表示第一個(gè)請(qǐng)求參數(shù)(參數(shù)返回值從右到左入棧)。
實(shí)際上,生成真正可執(zhí)行代碼時(shí),偽 SP、FP 會(huì)由物理 SP 寄存器加上偏移量替換。所以執(zhí)行過(guò)程中修改物理 SP,會(huì)引起偽 SP、FP 同步變化,比如執(zhí)行 SUBQ $16, SP 指令后,偽 SP 和偽 FP 都會(huì) -16。而且,反匯編二進(jìn)制而生成的匯編代碼中,只有物理 SP 寄存器。即 go tool objdump/go tool compile -S 輸出的匯編代碼中,沒(méi)有偽 SP 和 偽 FP 寄存器,只有物理 SP 寄存器。
另外還有 1 個(gè)比較特殊的偽寄存器:TLS:存儲(chǔ)當(dāng)前 goroutine 的 g 結(jié)構(gòu)體的指針。實(shí)際上,X86 和 AMD64 下的 TLS 是通過(guò)段寄存器 FS 或 GS 實(shí)現(xiàn)的線(xiàn)程本地存儲(chǔ)基地址,而當(dāng)前 g 的指針是線(xiàn)程本地存儲(chǔ)的第一個(gè)變量。
比如 github.com/petermattis/goid.Get 函數(shù)的匯編實(shí)現(xiàn)如下:
//funcGet()int64
TEXT·Get(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_goid(R14),R13
MOVQR13,ret+0(FP)
RET
編譯成二進(jìn)制之后,再通過(guò) go tool objdump 反編譯成匯編(Go 1.18),得到如下代碼:
TEXTgithub.com/petermattis/goid.Get.abi0(SB)/Users/bytedance/go/pkg/mod/github.com/petermattis/goid@v0.0.0-20221215004737-a150e88a970d/goid_go1.5_amd64.s
goid_go1.5_amd64.s:280x108adc0654c8b342530000000MOVQGS:0x30,R14
goid_go1.5_amd64.s:290x108adc94d8bae98000000MOVQ0x98(R14),R13
goid_go1.5_amd64.s:300x108add04c896c2408MOVQR13,0x8(SP)
goid_go1.5_amd64.s:310x108add5c3RET
可以知道 MOVQ (TLS), R14 指令最終編譯成了 MOVQ GS:0x30, R14 ,使用了 GS 段寄存器實(shí)現(xiàn)相關(guān)功能。
操作系統(tǒng)對(duì)內(nèi)存的一般劃分如下圖所示:
高地址+------------------+
||
|內(nèi)核空間|
||
--------------------
||
|棧|
||
--------------------
||
|.......|
||
--------------------
||
|堆|
||
--------------------
|全局?jǐn)?shù)據(jù)|
|------------------|
||
|靜態(tài)代碼|
||
|------------------|
|系統(tǒng)保留|
低地址|------------------|
這里提個(gè)疑問(wèn),我們知道協(xié)程分為有棧協(xié)程和無(wú)棧協(xié)程,go 語(yǔ)言是有棧協(xié)程。那你知道普通 gorutine 的調(diào)用棧是在哪個(gè)內(nèi)存區(qū)嗎?
1.3、函數(shù)調(diào)用棧幀
我們先熟悉幾個(gè)名詞。
caller:函數(shù)調(diào)用者。callee:函數(shù)被調(diào)用者。比如函數(shù) main 中調(diào)用 sum 函數(shù),那么 main 就是 caller,而 sum 函數(shù)就是 callee。棧幀:stack frame,即執(zhí)行中的函數(shù)所持有的、獨(dú)立連續(xù)的棧區(qū)段。一般用來(lái)保存函數(shù)參數(shù)、返回值、局部變量、返回 PC 值等信息。golang 的 ABI 規(guī)定,由 caller 管理函數(shù)參數(shù)和返回值。
下圖是 golang 的調(diào)用棧,源于曹春暉老師的 github 文章《匯編 is so easy》 ,做了簡(jiǎn)單修改:
caller
+------------------+
||
+---------------------->+------------------+
|||
||callerparentBP|
|BP(pseudoSP)+------------------+
|||
||LocalVar0|
|+------------------+
|||
||.......|
|+------------------+
|||
||LocalVarN|
+------------------+
callerstackframe||
|calleearg2|
|+------------------+
|||
||calleearg1|
|+------------------+
|||
||calleearg0|
|SP(RealRegister)->+------------------+--------------------------+FP(virtualregister)
||||
||returnaddr|parentreturnaddress|
+---------------------->+------------------+--------------------------+<-----------------------+?????????
????????????????????????????????????????????????????|??caller?BP???????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|??(caller?frame?pointer)??|????????????????????????????|?????????
?????????????????????????????????????BP(pseudo?SP)??+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|?????Local?Var0???????????|????????????????????????????|?????????
????????????????????????????????????????????????????+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|??????????????????????????????????????
????????????????????????????????????????????????????|?????Local?Var1???????????|??????????????????????????????????????
????????????????????????????????????????????????????+--------------------------+????????????????????callee?stack?frame
????????????????????????????????????????????????????|??????????????????????????|??????????????????????????????????????
????????????????????????????????????????????????????|???????.....??????????????|??????????????????????????????????????
????????????????????????????????????????????????????+--------------------------+????????????????????????????|?????????
????????????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
????????????????????????????????????????????????????|?????Local?VarN???????????|????????????????????????????|?????????
?????High?????????????????????????SP(Real?Register)?+--------------------------+????????????????????????????|?????????
??????^?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????|??????????????????????????|????????????????????????????|?????????
??????|?????????????????????????????????????????????+--------------------------+????<-----------------------+?????????
?????Low??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
??????????????????????????????????????????????????????????????callee
需要指出的是,上圖中的 CALLER BP 是在編譯期由編譯器在符合條件時(shí)自動(dòng)插入。所以手寫(xiě)匯編時(shí),計(jì)算 framesize 時(shí)不應(yīng)包括 CALLER BP 的空間。是否插入 CALLER BP 的主要判斷依據(jù)如下:
//Mustagreewithinternal/buildcfg.FramePointerEnabled.
constframepointer_enabled=GOARCH=="amd64"||GOARCH=="arm64"
以下是 Go 語(yǔ)言函數(shù)棧展開(kāi)邏輯的一段代碼,它側(cè)面驗(yàn)證了 BP 插入的條件:
- 函數(shù)的棧幀大小大于 0;
- 常量 framepointer_enabled 值為 true。
//Forarchitectureswithframepointers,ifthere's
//aframe,thenthere'sasavedframepointerhere.
//
//NOTE:Thiscodeisnotasgeneralasitlooks.
//Onx86,theABIistosavetheframepointerwordatthe
//topofthestackframe,sowehavetobackdownoverit.
//Onarm64,theframepointershouldbeatthebottomof
//thestack(withR29(akaFP)=RSP),inwhichcasewewould
//notwanttodothesubtractionhere.Butwestartedoutwithout
//anyframepointer,andwhenwewantedtoaddit,wedidn't
//wanttobreakalltheassemblydoingdirectwritesto8(RSP)
//tosetthefirstparametertoacalledfunction.
//SowedecidedtowritetheFPlink*below*thestackpointer
//(withR29=RSP-8inGofunctions).
//ThisistechnicallyABI-compatiblebutnotstandard.
//Andithappenstoendupmimickingthex86layout.
//Otherarchitecturesmaymakedifferentdecisions.
ifframe.varp>frame.sp&&framepointer_enabled{
frame.varp-=goarch.PtrSize
}
//Mustagreewithinternal/buildcfg.FramePointerEnabled.
constframepointer_enabled=GOARCH=="amd64"||GOARCH=="arm64"
1.4、golang常用匯編指令
參考文檔:
Go支持的 X86 指令
https://github.com/golang/arch/blob/v0.2.0/x86/x86.csv
Go支持的 ARM64 指令
https://github.com/golang/arch/blob/v0.2.0/arm64/arm64asm/inst.json
Go支持的 ARM 指令
https://github.com/golang/arch/blob/v0.2.0/arm/arm.csv
常用指令:

例如
MOVB$1, DI // 1 byte;將 DI 的第一個(gè) Byte 的值設(shè)置為 1
MOVW$0x10,BX//2bytes
MOVD$1,DX//4bytes
MOVQ$-10,AX//8bytes
SUBQ$0x18,SP//對(duì)SP做減法,擴(kuò)棧
ADDQ$0x18,SP//對(duì)SP做加法,縮棧
ADDQAX,BX//BX+=AX
SUBQAX,BX//BX-=AX
IMULQAX,BX//BX*=AX
JMPaddr//跳轉(zhuǎn)到地址,地址可為代碼中的地址,不過(guò)實(shí)際上手寫(xiě)一般不會(huì)出現(xiàn)
JMPlabel//跳轉(zhuǎn)到標(biāo)簽,可以跳轉(zhuǎn)到同一函數(shù)內(nèi)的標(biāo)簽位置
JMP2(PC)//向前轉(zhuǎn)2行
JMP-2(PC)//向后跳轉(zhuǎn)2行
JNZtarget//如果zeroflag被set過(guò),則跳轉(zhuǎn)
常用標(biāo)志位:

1.5 全局變量
參考文檔:《Go語(yǔ)言高級(jí)編程》的章節(jié) 3.3 常量和全局變量
https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-03-const-and-var.md
1.5.1 使用語(yǔ)法
使用 GLOBL 關(guān)鍵字聲明全局變量,用 DATA 定義指定內(nèi)存的值:
//DATA匯編指令指定對(duì)應(yīng)內(nèi)存中的值;width必須是1、2、4、8幾個(gè)寬度之一
DATAsymbol+offset(SB)/width,value//symbol+offset偏移量,width寬度,value初始值
//GLOBL指令聲明一個(gè)變量對(duì)應(yīng)的符號(hào),以及變量對(duì)應(yīng)的內(nèi)存大小
GLOBLsymbol(SB),flag,width//名為symbol,內(nèi)存寬度為width,flag可省略
例子:
DATAage+0x00(SB)/4,$18//age=18
GLOBLage(SB),RODATA,$4//聲明全局變量age,占用4Byte內(nèi)存空間
DATApi+0(SB)/8,$3.1415926
GLOBLpi(SB),RODATA,$8
DATAbio<>+0(SB)/8,$"hellowo"//<>表示只在當(dāng)前文件生效
DATAbio<>+8(SB)/8,$"old!!!!"//bio="helloworld!!!!"
GLOBLbio<>(SB),RODATA,$16
其中 flag 的字面量定義在 Go 標(biāo)準(zhǔn)庫(kù)下 src/runtime/textflag.h 文件中,需要在匯編文件中 #include "textflag.h",其類(lèi)型有有如下幾個(gè):
| flag | value | 說(shuō)明 |
|---|---|---|
| NOPROF | 1 | (TEXT項(xiàng)使用) 不優(yōu)化NOPROF標(biāo)記的函數(shù)。這個(gè)標(biāo)志已廢棄。(For TEXT items.) Don't profile the marked function. This flag is deprecated. |
| DUPOK | 2 | 在二進(jìn)制文件中允許一個(gè)符號(hào)的多個(gè)實(shí)例。鏈接器會(huì)選擇其中之一。It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use. |
| NOSPLIT | 4 | (TEXT項(xiàng)使用) 不插入檢測(cè)棧分裂(擴(kuò)張)的前導(dǎo)指令代碼(減少開(kāi)銷(xiāo),一般用于葉子節(jié)點(diǎn)函數(shù)(函數(shù)內(nèi)部不調(diào)用其他函數(shù)))。程序的棧幀中,如果調(diào)用其他函數(shù)會(huì)增加棧幀的大小,必須在棧頂留出可用空間。用來(lái)保護(hù)程序,例如堆棧拆分代碼本身。(For TEXT items.) Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself. |
| RODATA | 8 | (DATA和GLOBAL項(xiàng)使用) 將這個(gè)數(shù)據(jù)放在只讀的塊中。(For DATA and GLOBL items.) Put this data in a read-only section. |
| NOPTR | 16 | (用于DATA和GLOBL項(xiàng)目)這個(gè)數(shù)據(jù)不包含指針?biāo)跃筒恍枰占鱽?lái)掃描。(For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector. |
| WRAPPER | 32 | (TEXT項(xiàng)使用)這是包裝函數(shù) (For TEXT items.) This is a wrapper function and should not count as disabling recover. |
| NEEDCTXT | 64 | (TEXT項(xiàng)使用)此函數(shù)是一個(gè)閉包,因此它將使用其傳入的上下文寄存器。(For TEXT items.) This function is a closure so it uses its incoming context register. |
| TLSBSS | 256 | (用于DATA和GLOBL項(xiàng)目)將此數(shù)據(jù)放入線(xiàn)程本地存儲(chǔ)中。Allocate a word of thread local storage and store the offset from the thread local base to the thread local storage in this variable. |
| NOFRAME | 512 | (TEXT項(xiàng)使用)不要插入指令為此函數(shù)分配棧幀。僅在聲明幀大小為0的函數(shù)上有效。(函數(shù)必須是葉子節(jié)點(diǎn)函數(shù),且以0標(biāo)記堆棧函數(shù),沒(méi)有保存幀指針(或link寄存器架構(gòu)上的返回地址))TODO(mwhudson):目前僅針對(duì) ppc64x 實(shí)現(xiàn)。Do not insert instructions to allocate a stack frame for this function. Only valid on functions that declare a frame size of 0. TODO(mwhudson): only implemented for ppc64x at present. |
| REFLECTMETHOD | 1024 | 函數(shù)可以調(diào)用 reflect.Type.Method 或 reflect.Type.MethodByName。Function can call reflect.Type.Method or reflect.Type.MethodByName. |
| TOPFRAME | 2048 | (TEXT項(xiàng)使用)函數(shù)是調(diào)用堆棧的頂部。棧回溯應(yīng)在此功能處停止。Function is the outermost frame of the call stack. Call stack unwinders should stop at this function. |
| ABIWRAPPER | 4096 | 函數(shù)是一個(gè) ABI 包裝器。Function is an ABI wrapper. |
其中 NOSPLIT 需要特別注意,它表示該函數(shù)運(yùn)行不會(huì)導(dǎo)致棧分裂,用戶(hù)也可以使用 //go:nosplit 強(qiáng)制給 go 函數(shù)指定 NOSPLIT 屬性。例如:
//go:nosplit
funcsomeFunc(){
}
匯編中直接給函數(shù)標(biāo)記 NOSPLIT 即可:
//表示someFunc函數(shù)執(zhí)行時(shí)最多需要24字節(jié)本地變量和8字節(jié)參數(shù)空間
TEXT·someFunc(SB),NOSPLIT,$24-8
RET
鏈接器認(rèn)為標(biāo)記為 NOSPLIT 的函數(shù),最多需要使用 StackLimit 字節(jié)空間,所以不需要插入棧分裂(溢出)檢查,函數(shù)調(diào)用損耗更小。不過(guò),使用該標(biāo)志的時(shí)候要特別小心,萬(wàn)一發(fā)生意外容易導(dǎo)致棧溢出錯(cuò)誤,溢出時(shí)會(huì)在執(zhí)行期報(bào) nosplit stack overflow 錯(cuò)。Go 1.18 標(biāo)準(zhǔn)庫(kù)下 go/src/runtime/HACKING.md 中有如下說(shuō)明:
nosplitfunctions
MostfunctionsstartwithaprologuethatinspectsthestackpointerandthecurrentG'sstackboundandcallsmorestackifthestackneedstogrow.
Functionscanbemarked//go:nosplit(orNOSPLITinassembly)toindicatethattheyshouldnotgetthisprologue.Thishasseveraluses:
-Functionsthatmustrunontheuserstack,butmustnotcallintostackgrowth,forexamplebecausethiswouldcauseadeadlock,orbecausetheyhaveuntypedwordsonthestack.
-Functionsthatmustnotbepreemptedonentry.
-FunctionsthatmayrunwithoutavalidG.Forexample,functionsthatruninearlyruntimestart-up,orthatmaybeenteredfromCcodesuchascgocallbacksorthesignalhandler.
Splittablefunctionsensurethere'ssomeamountofspaceonthestackfornosplitfunctionstoruninandthelinkerchecksthatanystaticchainofnosplitfunctioncallscannotexceedthisbound.
Anyfunctionwitha//go:nosplitannotationshouldexplainwhyitisnosplitinitsdocumentationcomment.
另外,當(dāng)函數(shù)處于調(diào)用鏈的葉子節(jié)點(diǎn),且棧幀小于 StackSmall(128)字節(jié)時(shí),則自動(dòng)標(biāo)記為 NOSPLIT。此邏輯的代碼如下:
//constStackSmall=128
ifctxt.Arch.Family==sys.AMD64&&autoffsettrue
LeafSearch:
forq:=p;q!=nil;q=q.Link{
switchq.As{
caseobj.ACALL:
//Treatcommonruntimecallsthattakenoarguments
//thesameasduffcopyandduffzero.
if!isZeroArgRuntimeCall(q.To.Sym){
leaf=false
breakLeafSearch
}
fallthrough
caseobj.ADUFFCOPY,obj.ADUFFZERO:
ifautoffset>=objabi.StackSmall-8{
leaf=false
breakLeafSearch
}
}
}
ifleaf{
p.From.Sym.Set(obj.AttrNoSplit,true)
}
}
1.5.2 Go 語(yǔ)言中的常用用法
在匯編代碼中使用 go 變量:
#include"textflag.h"
TEXT·get(SB),NOSPLIT,$0-8
MOVQ·a(SB),AX//把go代碼定義的全局變量讀到AX中
MOVQAX,ret+0(FP)//把AX的值寫(xiě)入返回值位置
RET
packagemain
vara=999
funcget()intfuncmain(){
println(get())
}
go 代碼中使用匯編定義的變量:
// string 定義形式 1:在 String 結(jié)構(gòu)體后多分配一個(gè)[n]byte 數(shù)組存放靜態(tài)字符串
DATA·Name+0(SB)/8,$·Name+16(SB)//StringHeader.Data
DATA·Name+8(SB)/8,$6//StringHeader.Len
DATA·Name+16(SB)/8,$"gopher"//[6]byte{'g','o','p','h','e','r'}
GLOBL·Name(SB),NOPTR,$24//struct{Datauintptr,Lenint,str[6]byte}
// string 定義形式 2:獨(dú)立分配一個(gè)僅當(dāng)前文件可見(jiàn)的[n]byte 數(shù)組存放靜態(tài)字符串
DATAstr<>+0(SB)/8,$"HelloWo"//str[0:8]={'H','e','l','l','o','','W','o'}
DATAstr<>+8(SB)/8,$"rld!"//str[9:12]={'r','l','d','!''}
GLOBLstr<>(SB),NOPTR,$16//定義全局?jǐn)?shù)組varstr<>[16]byte
DATA·Helloworld+0(SB)/8,$str<>(SB)//StringHeader.Data=&str<>
DATA·Helloworld+8(SB)/8,$12//StringHeader.Len=12
GLOBL·Helloworld(SB),NOPTR,$16//struct{Datauintptr,Lenint}
varName,Helloworldstring
funcdoSth(){
fmt.Printf("Name:%s
",Name)//讀取匯編中初始化的變量Name
fmt.Printf("Helloworld:%s
",Helloworld)//讀取匯編中初始化的變量Helloworld
}
//輸出:
//Name:gopher
//Helloworld:HelloWorld!
1.6 函數(shù)調(diào)用
1.6.1 使用語(yǔ)法
Go 語(yǔ)言匯編中,函數(shù)聲明格式如下:
告訴匯編器該數(shù)據(jù)放到TEXT區(qū)
^靜態(tài)基地址指針(告訴匯編器這是基于靜態(tài)地址的數(shù)據(jù))
|^
||標(biāo)簽函數(shù)入?yún)?返回值占用空間大小
||^^
||||
TEXTpkgname·funcname(SB),TAG,$16-24
^^^^
||||
函數(shù)所屬包名函數(shù)名表示ABI類(lèi)型函數(shù)棧幀大小(本地變量占用空間大小)

一些說(shuō)明:
- 棧幀大小包括局部變量和可能需要的額外調(diào)用函數(shù)的參數(shù)空間的總大小,但不不包含調(diào)用其他函數(shù)時(shí)的 ret address 的大小。
- 匯編文件中,函數(shù)名以 '·' 開(kāi)頭或連接 pkgname 是固定格式。
- go 函數(shù)采用的是 caller-save 模式,被調(diào)用者的參數(shù)、返回值、棧位置都由調(diào)用者維護(hù)。
go 語(yǔ)言編譯成匯編:
gotoolcompile-Sxxx.go
gobuild-gcflags-Sxxx.go
從二進(jìn)制反編譯為匯編:
gotoolobjdump-s"main.main"main.out>main.S
1.6.2 使用例子
Go 函數(shù)調(diào)用匯編函數(shù):
//add.go
packagemain
import"fmt"
funcadd(x,yint64)int64
funcmain(){
fmt.Println(add(2,3))
}
//add_amd64.s
//add(x,y)->x+y
TEXT·add(SB),NOSPLIT,$0
MOVQx+0(FP),BX
MOVQy+8(FP),BP
ADDQBP,BX
MOVQBX,ret+16(FP)
RET
匯編調(diào)用 go 語(yǔ)言函數(shù):
packagemain
import"fmt"
funcadd(x,yint)int{
returnx+y
}
funcoutput(a,bint)int
funcmain(){
s:=output(10,13)
fmt.Println(s)
}
#include"textflag.h"
//funcoutput(a,bint)int
TEXT·output(SB),NOSPLIT,$24-24
MOVQa+0(FP),DX//arga
MOVQDX,0(SP)//argx
MOVQb+8(FP),CX//argb
MOVQCX,8(SP)//argy
CALL·add(SB)//在調(diào)用add之前,已經(jīng)把參數(shù)都通過(guò)物理寄存器SP搬到了函數(shù)的棧頂
MOVQ16(SP),AX//add函數(shù)會(huì)把返回值放在這個(gè)位置
MOVQAX,ret+16(FP)//returnresult
RET
1.6.1 匯編函數(shù)中用到的一些特殊命令(偽指令)
GO_RESULTS_INITIALIZED:如果 Go 匯編函數(shù)返回值含指針,則該指針信息必須由 Go 源文件中的函數(shù)的 Go 原型提供,即使對(duì)于未直接從 Go 調(diào)用的匯編函數(shù)也是如此。如果返回值將在調(diào)用指令期間保存實(shí)時(shí)指針,則該函數(shù)中應(yīng)首先將結(jié)果歸零, 然后執(zhí)行偽指令 GO_RESULTS_INITIALIZED。表明該堆棧位置應(yīng)該執(zhí)行進(jìn)行 GC 掃描,避免其指向的內(nèi)存地址唄 GC 意外回收。
NO_LOCAL_POINTERS: 就是字面意思,表示函數(shù)沒(méi)有指針類(lèi)型的局部變量。
PCDATA: Go 語(yǔ)言生成的匯編,利用此偽指令表明匯編所在的原始 Go 源碼的位置(file&line&func),用于生成 PC 表格。runtime.FuncForPC 函數(shù)就是通過(guò) PC 表格得到結(jié)果的。一般由編譯器自動(dòng)插入,手動(dòng)維護(hù)并不現(xiàn)實(shí)。
FUNCDATA: 和 PCDATA 的格式類(lèi)似,用于生成 FUNC 表格。FUNC 表格用于記錄函數(shù)的參數(shù)、局部變量的指針信息,GC 依據(jù)它來(lái)跟蹤棧中指針指向內(nèi)存的生命周期,同時(shí)棧擴(kuò)縮容的時(shí)候也是依據(jù)它來(lái)確認(rèn)是否需要調(diào)整棧指針的值(如果指向的地址在需要擴(kuò)縮容的棧中,則需要同步修改)。
1.7 條件編譯
Go 語(yǔ)言?xún)H支持有限的條件編譯規(guī)則:
- 根據(jù)文件名編譯。
- 根據(jù) build 注釋編譯。
根據(jù)文件名編譯類(lèi)似 *_test.go,通過(guò)添加平臺(tái)后綴區(qū)分,比如: asm_386.s、asm_amd64.s、asm_arm.s、asm_arm64.s、asm_mips64x.s、asm_linux_amd64.s、asm_bsd_arm.s 等.
根據(jù) build 注釋編譯,就是在源碼中加入?yún)^(qū)分平臺(tái)和編譯器版本的注釋。比如:
//go:build(darwin||freebsd||netbsd||openbsd)&&gc
//+builddarwinfreebsdnetbsdopenbsd
//+buildgc
Go 1.17 之前,我們可以通過(guò)在源碼文件頭部放置 +build 構(gòu)建約束指示符來(lái)實(shí)現(xiàn)構(gòu)建約束,但這種形式十分易錯(cuò),并且它并不支持&&和||這樣的直觀的邏輯操作符,而是用逗號(hào)、空格替代,下面是原 +build 形式構(gòu)建約束指示符的用法及含義:

Go 1.17 引入了 //go:build 形式的構(gòu)建約束指示符,支持&&和||邏輯操作符,如下代碼所示:
//go:buildlinux&&(386||amd64||arm||arm64||mips64||mips64le||ppc64||ppc64le)
//go:buildlinux&&(mips64||mips64le)
//go:buildlinux&&(ppc64||ppc64le)
//go:buildlinux&&!386&&!arm
考慮到兼容性,Go 命令可以識(shí)別這兩種形式的構(gòu)建約束指示符,但推薦 Go 1.17 之后都用新引入的這種形式。
gofmt 可以兼容處理兩種形式,處理原則是:如果一個(gè)源碼文件只有 // +build 形式的指示符,gofmt 會(huì)將與其等價(jià)的 //go:build 行加入。否則,如果一個(gè)源文件中同時(shí)存在這兩種形式的指示符行,那么 //+build 行的信息將被 //go:build 行的信息所覆蓋。
2、 go 語(yǔ)言 ABI
參考文檔:
Go internal ABI specification
https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md
Proposal: Create an undefined internal calling convention
https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md
名詞解釋?zhuān)?/span>ABI: application binary interface, 應(yīng)用程序二進(jìn)制接口,規(guī)定了程序在機(jī)器層面的操作規(guī)范和調(diào)用規(guī)約。調(diào)用規(guī)約: calling convention, 所謂“調(diào)用規(guī)約”是調(diào)用方和被調(diào)用方對(duì)于函數(shù)調(diào)用的一個(gè)明確的約定,包括:函數(shù)參數(shù)與返回值的傳遞方式、傳遞順序。只有雙方都遵守同樣的約定,函數(shù)才能被正確地調(diào)用和執(zhí)行。如果不遵守這個(gè)約定,函數(shù)將無(wú)法正確執(zhí)行。
Go 從1.17.1版本開(kāi)始支持多 ABI:1. 為了兼容性各平臺(tái)保持通用性,保留歷史版本 ABI,并更名為 ABI0。2. 為了更好的性能,增加新版本 ABI 取名 ABIInternal。ABI0 遵循平臺(tái)通用的函數(shù)調(diào)用約定,實(shí)現(xiàn)簡(jiǎn)單,不用擔(dān)心底層cpu架構(gòu)寄存器的差異;ABIInternal 可以指定特定的函數(shù)調(diào)用規(guī)范,可以針對(duì)特定性能瓶頸進(jìn)行優(yōu)化,在多個(gè) Go 版本之間可以迭代,靈活性強(qiáng),支持寄存器傳參提升性能。Go 匯編為了兼容已存在的匯編代碼,保持使用舊的 ABI0。
Go 為什么在有了 ABI0 之后,還要引入 ABIInternal?當(dāng)然是為了性能!據(jù)官方測(cè)試,寄存器傳參可以帶來(lái) 5% 的性能提升。
我們看一個(gè)例子:
packagemain
import_"fmt"
funcPrint(deltastring)
funcmain(){
Print("hello")
}
#include"textflag.h"
TEXT·Print(SB),NOSPLIT,$8
CALLfmt·Println(SB)
RET
運(yùn)行上面代碼會(huì)報(bào)錯(cuò):main.Print: relocation target fmt.Println not defined for ABI0 (but is defined for ABIInternal)
原因是,fmt·Println 函數(shù)默認(rèn)使用的 ABI 標(biāo)準(zhǔn)是 ABIInternal,而 Go 語(yǔ)言手寫(xiě)的匯編使用的 ABI 格式是 ABI0,二者標(biāo)準(zhǔn)不一樣不能直接調(diào)用。不過(guò) Go 語(yǔ)言可以通過(guò) //go:linkname 的方式為 ABIInternal 生成 ABI0 包裝。
packagemain
import(
"fmt"
)
//go:linknamePrintlnfmt.Println
funcPrintln(a...any)(nint,errerror)
funcPrint(deltainterface{})funcmain(){
Print("hello")
}
#include"textflag.h"
TEXT·Print(SB),NOSPLIT,$48-16
LEAQstrp+0(FP),AX
MOVQAX,0(SP)//[]interface{}slice的pointer
MOVQ$1,BX
MOVQBX,8(SP)//slice的len
MOVQBX,16(SP)//slice的cap
CALLfmt·Println(SB)////go:linkname為fmt.Println生成一個(gè)ABI0包裝后,匯編可以直接調(diào)用
RET
簡(jiǎn)單說(shuō)明:函數(shù) fmt.Println 是一個(gè)變參函數(shù),變參(a ...any)實(shí)際上是 (a []any)的語(yǔ)法糖。參數(shù)中,slice 占 24Byte,int 占 8Byte,error 是 interface 類(lèi)型,占 16Byte,加起來(lái)是 48 Byte。所以,調(diào)用此函數(shù)時(shí),caller 需要再棧上準(zhǔn)備 24Byte 空間。而 Print 的入?yún)偤檬且粋€(gè) interface{} 類(lèi)型,和 any 一致,所以只要把 Print 函數(shù)的入?yún)⒌牡刂焚x給 a 的指針,并把 a 的 len 和 cap 設(shè)置為 1,就可以調(diào)用 fmt·Println 函數(shù)了。如以上代碼所示。
3、內(nèi)存管理和 GC 對(duì)匯編的影響
3.1 調(diào)用棧擴(kuò)縮容對(duì)匯編的影響
為了減少對(duì)內(nèi)存的占用,goroutine 啟動(dòng)時(shí) runtime 只給它分配了很少的棧內(nèi)存。所有函數(shù)(標(biāo)記 go:nosplit 的除外)的序言部分(啟動(dòng)指令)會(huì)插入分段檢查,當(dāng)發(fā)現(xiàn)棧溢出(??臻g不足)時(shí),就會(huì)調(diào)用 runtime.morestack,執(zhí)行棧拓展邏輯:
- 舊版本的 Go 編譯器采用了分段棧機(jī)制實(shí)現(xiàn)棧拓展,當(dāng)一個(gè) goroutine 的執(zhí)行棧溢出時(shí),就增加一個(gè)棧內(nèi)存作為調(diào)用棧的補(bǔ)充,新舊棧彼此沒(méi)有連續(xù)。這種設(shè)計(jì)的缺陷很容易破壞緩存的局部性原理,從而降低程序的運(yùn)行時(shí)性能。
- Go 1.3 版本開(kāi)始,引入了連續(xù)棧(拷貝棧)機(jī)制,并把 goroutine 的初始棧大小由 8KB 降低到了 2KB。當(dāng)一個(gè)執(zhí)行棧發(fā)生溢出時(shí),新建一個(gè)兩倍于原棧大小的新棧,并將原棧整個(gè)拷貝到新棧上,保證整個(gè)棧是連續(xù)的。
棧的拷貝有些副作用:
- 如果棧上存在指向當(dāng)前被拷貝棧的指針,當(dāng)??截悎?zhí)行完成后,這個(gè)指針還是指向原棧,需要更新。
- goroutine 的 g 結(jié)構(gòu)體上的 gobuf 成員也還是指向舊的棧,也需要更新。
除了正在拷貝的棧中可能存在指向自己的的指針外,還有沒(méi)有其他存活中的內(nèi)存有指向即將失效的??臻g的指針呢?答案在 go 逃逸分析源碼 中,代碼如下:
//Escapeanalysis.
//
//HereweanalyzefunctionstodeterminewhichGovariables
//(includingimplicitallocationssuchascallsto"new"or"make",
//compositeliterals,etc.)canbeallocatedonthestack.Thetwo
//keyinvariantswehavetoensureare:(1)pointerstostackobjects
//cannotbestoredintheheap,and(2)pointerstoastackobject
//cannotoutlivethatobject(e.g.,becausethedeclaringfunction
//returnedanddestroyedtheobject'sstackframe,oritsspaceis
//reusedacrossloopiterationsforlogicallydistinctvariables).
//
其中 “(1) pointers to stack objects cannot be stored in the heap” 表明指向棧對(duì)象的指針不能存儲(chǔ)在堆中。
拷貝棧理論上沒(méi)有上限,但是一般都設(shè)置了上限。當(dāng)新的棧大小超過(guò)了 maxstacksize 就會(huì)拋出”stack overflow“的異常。maxstacksize 是在 runtime.main 中設(shè)置的。64 位系統(tǒng)下棧的最大值 1GB、32 位系統(tǒng)是 250MB。參考代碼:
ifnewsize>maxstacksize||newsize>maxstackceiling{
ifmaxstacksizeprint("runtime:goroutinestackexceeds",maxstacksize,"-bytelimit
")
}else{
print("runtime:goroutinestackexceeds",maxstackceiling,"-bytelimit
")
}
print("runtime:sp=",hex(sp),"stack=[",hex(gp.stack.lo),",",hex(gp.stack.hi),"]
")
throw("stackoverflow")
}
由拷貝棧的原理可知,拷貝棧對(duì) Go 匯編是透明的。
3.2 GC 對(duì)匯編的影響
由于 GC 會(huì)動(dòng)態(tài)回收沒(méi)有被引用的堆內(nèi)存,而 goroutine 的調(diào)用棧在堆空間,所以如果調(diào)用棧中存了堆內(nèi)存的指針,就需要告訴 GC 棧中含指針。上文中說(shuō)到的偽指令 FUNCDATA、GO_RESULTS_INITIALIZED、NO_LOCAL_POINTERS 就是干這個(gè)事的。由于 FUNCDATA 偽指令幾乎只能由編譯器維護(hù),所以在手寫(xiě)的匯編函數(shù)本地內(nèi)存棧中保存指向動(dòng)態(tài)內(nèi)存的指針幾乎是一種奢望。
4、 函數(shù)內(nèi)聯(lián)和匯編
參考文檔:
Go: Inlining Strategy & Limitation
https://medium.com/a-journey-with-go/go-inlining-strategy-limitation-6b6d7fc3b1be
4.1 查看內(nèi)聯(lián)情況
可以通過(guò)執(zhí)行以下命令,輸出被內(nèi)聯(lián)的函數(shù):
gobuild-gcflags="-m"main.go
#輸出結(jié)果:
#./op.go6:caninlineadd
#./op.go6:caninlinesub
#./main.go11:inliningcalltosub
#./main.go11:inliningcalltoadd
#./main.go12:inliningcalltofmt.Printf
或者使用參數(shù) -gflags="-m -m" 運(yùn)行,查看編譯器的詳細(xì)優(yōu)化策略:
gobuild-gcflags="-m-m"main.go
輸出很詳細(xì):
#command-line-arguments
./main.go6:cannotinlinemain:functiontoocomplex:cost106exceedsbudget80
./main.go12:inliningcalltofmt.Printf
./main.go6:caninlinetoEfacewithcost0as:func(){}
./main.go2:shlxescapestoheap:
./main.go2:flow:i=&{storageforshlx}:
./main.go2:fromshlx(spill)at./main.go2
./main.go2:fromi=shlx(assign)at./main.go4
./main.go2:flow:{storagefor...argument}=i:
./main.go2:from...argument(slice-literal-element)at./main.go12
./main.go2:flow:fmt.a=&{storagefor...argument}:
./main.go2:from...argument(spill)at./main.go12
./main.go2:fromfmt.format,fmt.a:="%+v",...argument(assign-pair)at./main.go12
./main.go2:flow:{heap}=*fmt.a:
./main.go2:fromfmt.Fprintf(io.Writer(os.Stdout),fmt.format,fmt.a...)(callparameter)at./main.go12
./main.go2:xescapestoheap:
./main.go2:flow:i=&{storageforx}:
./main.go2:fromx(spill)at./main.go2
./main.go2:fromi=x(assign)at./main.go4
./main.go2:flow:{storagefor...argument}=i:
./main.go2:from...argument(slice-literal-element)at./main.go12
./main.go2:flow:fmt.a=&{storagefor...argument}:
./main.go2:from...argument(spill)at./main.go12
./main.go2:fromfmt.format,fmt.a:="%+v",...argument(assign-pair)at./main.go12
./main.go2:flow:{heap}=*fmt.a:
./main.go2:fromfmt.Fprintf(io.Writer(os.Stdout),fmt.format,fmt.a...)(callparameter)at./main.go12
./main.go2:xescapestoheap
./main.go2:shlxescapestoheap
./main.go12:...argumentdoesnotescape
Go 編譯器默認(rèn)將進(jìn)行內(nèi)聯(lián)優(yōu)化,可以通過(guò) -gcflags="-l" 選項(xiàng)全局禁用內(nèi)聯(lián),與一個(gè)-l禁用內(nèi)聯(lián)相反,如果傳遞兩個(gè)或兩個(gè)以上的-l則會(huì)打開(kāi)內(nèi)聯(lián),并啟用更激進(jìn)的內(nèi)聯(lián)策略。例如以下代碼:
//3.1:varclosure=NewClosure()
funcmain(){
//3.2:varclosurefunc()int
varclosure=NewClosure()
closure()
//3.3:closure=NewClosure()
closure()
}
funcNewClosure()func()int{
i:=0returnfunc()int{
i++
returni
}
}
命令 go build -gcflags="-m" main.go 和 go build -gcflags="-m -l -l" main.go 都是輸出:
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go26:inliningcalltoNewClosure
./main.go9:caninlinemain.func1
./main.go9:inliningcalltomain.func1
./main.go9:inliningcalltomain.func1
./main.go26:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
命令 go build -gcflags="-m" main.go 輸出:
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
4.2 內(nèi)聯(lián)前后性能對(duì)比
首先,看一下函數(shù)內(nèi)聯(lián)與非內(nèi)聯(lián)的性能差異。內(nèi)聯(lián)可以避免函數(shù)調(diào)用過(guò)程中的一些開(kāi)銷(xiāo):創(chuàng)建棧幀,讀寫(xiě)寄存器。不過(guò),對(duì)函數(shù)體進(jìn)行拷貝也會(huì)增大二進(jìn)制文件的大小。據(jù) Go 官方宣傳,內(nèi)聯(lián)大概會(huì)有 5~6% 的性能提升。
//go:noinline
funcmaxNoinline(a,bint)int{
ifareturnb
}
returna
}
funcmaxInline(a,bint)int{
ifareturnb
}
returna
}
funcBenchmarkInline(b*testing.B){
x,y:=1,2
b.Run("BenchmarkNoInline",func(b*testing.B){
b.ResetTimer()
fori:=0;i"BenchmarkInline",func(b*testing.B){
b.ResetTimer()
fori:=0;i
在程序代碼中,想要禁止編譯器內(nèi)聯(lián)優(yōu)化很簡(jiǎn)單,在函數(shù)定義前一行添加 //go:noinline 即可。以下是性能對(duì)比結(jié)果:
BenchmarkInline/BenchmarkNoInline-128861373981.248ns/op0B/op0allocs/op
BenchmarkInline/BenchmarkInline-1210000000000.2506ns/op0B/op0allocs/op
因?yàn)楹瘮?shù)體內(nèi)部的執(zhí)行邏輯非常簡(jiǎn)單,此時(shí)內(nèi)聯(lián)與否的性能差異主要體現(xiàn)在函數(shù)調(diào)用的固定開(kāi)銷(xiāo)上。顯而易見(jiàn),該差異是非常大的。
4.3 內(nèi)聯(lián)條件
Go 語(yǔ)言代碼函數(shù)內(nèi)聯(lián)的策略每個(gè)編譯器版本都有細(xì)微差別,比如新版已支持含 for 和 閉包 的函數(shù)內(nèi)聯(lián)。1.18 版本的部分無(wú)法內(nèi)聯(lián)的規(guī)則如下:
- 函數(shù)標(biāo)注 "go:noinline" 注釋。
- 函數(shù)標(biāo)注 "go:norace" 注釋?zhuān)沂褂?"-gcflags=-d checkptr" 參數(shù)編譯。
- 函數(shù)標(biāo)注 "go:cgo_unsafe_args" 注釋。
- 函數(shù)標(biāo)注 "go:uintptrescapes" 注釋。
- 函數(shù)只有聲明而沒(méi)有函數(shù)體:比如函數(shù)實(shí)體在匯編文件 xxx.s 中。
- 超過(guò)小代碼量邊界的函數(shù):內(nèi)聯(lián)的小代碼量邊界是 80 個(gè)節(jié)點(diǎn)(抽象語(yǔ)法樹(shù)AST的節(jié)點(diǎn))。
- 函數(shù)中含某些關(guān)鍵字的函數(shù):比如 select、defer、go、recover 等。
- 一些特殊的內(nèi)部函數(shù):比如 runtime.getcallerpc、runtime.getcallersp (這倆太特殊了)。
- 函數(shù)內(nèi)部使用 type 關(guān)鍵字重定義了類(lèi)型:比如 "type Int int" 或 "type Int = int"。
- 作為尾遞歸調(diào)用時(shí)。
此外,還有一些編譯器覺(jué)得內(nèi)聯(lián)成本很低,所以必然內(nèi)聯(lián)的函數(shù):
- "runtime" package 下的 "heapBits.nextArena" 和 "builtin" package 下的 "append"。
- "encoding/binary" package 下的:"littleEndian.Uint64", "littleEndian.Uint32", "littleEndian.Uint16","bigEndian.Uint64", "bigEndian.Uint32", "bigEndian.Uint16","littleEndian.PutUint64", "littleEndian.PutUint32", "littleEndian.PutUint16","bigEndian.PutUint64", "bigEndian.PutUint32", "bigEndian.PutUint16", "append"。
由規(guī)則 5 可知,Go 語(yǔ)言匯編是無(wú)法內(nèi)聯(lián)的。
此外,關(guān)于閉包內(nèi)聯(lián)是一個(gè)比較復(fù)雜的話(huà)題,據(jù)筆者測(cè)試,1.18 有如上規(guī)則:
- 滿(mǎn)足條件的閉包可以?xún)?nèi)聯(lián)。
- 閉包通用部分在內(nèi)聯(lián)統(tǒng)計(jì)的時(shí)候,占用函數(shù)的 15 個(gè) AST 節(jié)點(diǎn)。
- 變量保存的閉包,如果是局部變量且沒(méi)有重新賦值過(guò),則可以被內(nèi)聯(lián)。
關(guān)于閉包內(nèi)聯(lián)的第 3 條規(guī)則,有如下例子:
//3.1:varclosure=NewClosure()
funcmain(){
//3.2:varclosurefunc()int
varclosure=NewClosure()
closure()
//3.3:closure=NewClosure()
closure()
}
funcNewClosure()func()int{
i:=0
returnfunc()int{
i++
returni
}
}
執(zhí)行 go build -gcflags="-m" ./ 輸出如下
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go26:inliningcalltoNewClosure
./main.go9:caninlinemain.func1
./main.go9:inliningcalltomain.func1
./main.go9:inliningcalltomain.func1
./main.go26:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
表明閉包 closure 可以?xún)?nèi)聯(lián)。如果把 3.1 或 3.2 或 3.3 的注釋打開(kāi),則將會(huì)輸出:
./main.go6:caninlineNewClosure
./main.go9:caninlineNewClosure.func1
./main.go22:inliningcalltoNewClosure
./main.go22:funcliteraldoesnotescape
./main.go2:movedtoheap:i
./main.go9:funcliteralescapestoheap
表明閉包 closure 無(wú)法內(nèi)聯(lián)。
此外,如果想禁用閉包內(nèi)聯(lián),可以使用 -gcflags="-d=inlfuncswithclosures=0" 或-gcflags="-d inlfuncswithclosures=0" 參數(shù)編譯。
gobuild-gcflags="-d=inlfuncswithclosures=0"main.go
gobuild-gcflags="-dinlfuncswithclosures=0"main.go
如果想了解 go 1.18 的內(nèi)聯(lián)檢查邏輯,可以看這個(gè)源碼:inline.CanInline 和 (*inline.hairyVisitor).doNode。其調(diào)用順序是:inline.CanInline --> inline.hairyVisitor.tooHairy --> inline.hairyVisitor.doNode。
//CanInlinedetermineswhetherfnisinlineable.
//Ifso,CanInlinesavescopiesoffn.Bodyandfn.Dclinfn.Inl.
//fnandfn.Bodywillalreadyhavebeentypechecked.
funcCanInline(fn*ir.Func){
...
//Ifmarked"go:noinline",don'tinline
iffn.Pragma&ir.Noinline!=0{
reason="markedgo:noinline"
return
}
//Ifmarked"go:norace"and-racecompilation,don'tinline.
ifbase.Flag.Race&&fn.Pragma&ir.Norace!=0{
reason="markedgo:noracewith-racecompilation"
return
}
//Ifmarked"go:nocheckptr"and-dcheckptrcompilation,don'tinline.
ifbase.Debug.Checkptr!=0&&fn.Pragma&ir.NoCheckPtr!=0{
reason="markedgo:nocheckptr"
return
}
//Ifmarked"go:cgo_unsafe_args",don'tinline,sincethe
//functionmakesassumptionsaboutitsargumentframelayout.
iffn.Pragma&ir.CgoUnsafeArgs!=0{
reason="markedgo:cgo_unsafe_args"
return
}
//Ifmarkedas"go:uintptrescapes",don'tinline,sincethe
//escapeinformationislostduringinlining.
iffn.Pragma&ir.UintptrEscapes!=0{
reason="markedashavinganescapinguintptrargument"
return
}
//Thenowritebarrierreccheckercurrentlyworksatfunction
//granularity,soinliningyeswritebarrierrecfunctionscan
//confuseit(#22342).Asaworkaround,disallowinlining
//themfornow.
iffn.Pragma&ir.Yeswritebarrierrec!=0{
reason="markedgo:yeswritebarrierrec"
return
}
//Iffnhasnobody(isdefinedoutsideofGo),cannotinlineit.
iflen(fn.Body)==0{
reason="nofunctionbody"
return
}
...
visitor:=hairyVisitor{
budget:inlineMaxBudget,//inlineMaxBudget==80
extraCallCost:cc,
}
ifvisitor.tooHairy(fn){
reason=visitor.reason
return
}
...
}
func(v*hairyVisitor)tooHairy(fn*ir.Func)bool{
v.do=v.doNode//cacheclosure
ifir.DoChildren(fn,v.do){
returntrue
}
...
}
func(v*hairyVisitor)doNode(nir.Node)bool{
...
caseir.OSELECT,
ir.OGO,
ir.ODEFER,
ir.ODCLTYPE,//can'tprintyet
ir.OTAILCALL:
v.reason="unhandledop"+n.Op().String()
returntrue
...
}
5、 有哪些有意思的使用場(chǎng)景
5.1、 獲取 goid
goid 即 goroutine id,最常用三方庫(kù)應(yīng)該就是 petermattis/goid, 里通過(guò)匯編獲取 goid 的代碼關(guān)鍵邏輯如下:
runtime_go1.9.go 代碼:
//go:buildgc&&go1.9//+buildgc,go1.9packagegoid
typestackstruct{
louintptr
hiuintptr
}
typegobufstruct{
spuintptr
pcuintptr
guintptr
ctxtuintptr
retuintptr
lruintptr
bpuintptr
}
typegstruct{
stackstack
stackguard0uintptr
stackguard1uintptr
_panicuintptr
_deferuintptr
muintptr
schedgobuf
syscallspuintptr
syscallpcuintptr
stktopspuintptr
paramuintptr
atomicstatusuint32
stackLockuint32
goidint64//Hereitis!
}
goid_go1.5_amd64.go 代碼:
//go:build(amd64||amd64p32)&&gc&&go1.5//+buildamd64amd64p32//+buildgc//+buildgo1.5packagegoid
funcGet()int64
goid_go1.5_amd64.s 代碼:
//go:build(amd64||amd64p32)&&gc&&go1.5
//+buildamd64amd64p32
//+buildgc
//+buildgo1.5
#include"go_asm.h"
#include"textflag.h"
//funcGet()int64
TEXT·Get(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_goid(R14),R13
MOVQR13,ret+0(FP)
RET
不過(guò)這樣獲取 goid 有一個(gè)局限性,就是如果當(dāng)前處于 g0 調(diào)用棧(系統(tǒng)調(diào)用或CGO函數(shù)中)時(shí),拿到的不是當(dāng)前 g 的 goid,而是 是 g0 的 goid。在這種情況下 g.m.curg.goid 才是當(dāng)前 g 的 goid。參考Go1.18 標(biāo)準(zhǔn)庫(kù)下go/src/runtime/HACKING.md 文件里的說(shuō)明:
getg()andgetg().m.curg
Togetthecurrentuserg,usegetg().m.curg.
getg()alonereturnsthecurrentg,butwhenexecutingonthesystemorsignalstacks,thiswillreturnthecurrentM's"g0"or"gsignal",respectively.Thisisusuallynotwhatyouwant.
Todetermineifyou'rerunningontheuserstackorthesystemstack,usegetg()==getg().m.curg.
除了 goid,pid也可以用匯編獲?。篶holeraehyq/pid 是一個(gè) fork petermattis/goid 的倉(cāng)庫(kù),里面增加了獲取 pid 的實(shí)現(xiàn),實(shí)現(xiàn)代碼如下:
p_m_go1.19.go 代碼:
//go:buildgc&&go1.19&&!go1.21//+buildgc,go1.19,!go1.21packagegoid
typepstruct{
idint32//Hereispid
}
typemstruct{
g0uintptr//goroutinewithschedulingstack
morebufgobuf//gobufargtomorestack
divmoduint32//div/moddenominatorforarm-knowntoliblink
_uint32//Fieldsnotknowntodebuggers.
prociduint64//fordebuggers,butoffsetnothard-coded
gsignaluintptr//signal-handlingg
goSigStackgsignalStack//Go-allocatedsignalhandlingstack
sigmasksigset//storageforsavedsignalmask
tls[6]uintptr//thread-localstorage(forx86externregister)
mstartfnfunc()
curguintptr//currentrunninggoroutine
caughtsiguintptr//goroutinerunningduringfatalsignal
p*p//attachedpforexecutinggocode(nilifnotexecutinggocode)
}
pid_go1.5.go 代碼:
//go:build(amd64||amd64p32||arm64)&&!windows&&gc&&go1.5//+buildamd64amd64p32arm64//+build!windows//+buildgc//+buildgo1.5packagegoid
//go:nosplitfuncgetPid()uintptr//go:nosplitfuncGetPid()int{
returnint(getPid())
}
pid_go1.5_amd64.s 代碼:
//+buildamd64amd64p32
//+buildgc,go1.5
#include"go_asm.h"
#include"textflag.h"
//funcgetPid()int64
TEXT·getPid(SB),NOSPLIT,$0-8
MOVQ(TLS),R14
MOVQg_m(R14),R13
MOVQm_p(R13),R14
MOVLp_id(R14),R13
MOVQR13,ret+0(FP)
RET
不過(guò),通過(guò)這種方式獲取的 pid 也有一個(gè)局限性:在持有 pid 之后的時(shí)間里,可能當(dāng)前 goroutine 已經(jīng)被調(diào)度到其他 P 上了,也就是在使用 pid 的時(shí)候當(dāng)前 pid 已經(jīng)改變了。如果想要持有在持有 pid 的過(guò)程中持續(xù)幫當(dāng)當(dāng)前 P,可以使用一下方式:
import"unsafe"
var_=unsafe.Sizeof(0)
//go:linknameprocPinruntime.procPin
//go:nosplit
funcprocPin()int
//go:linknameprocUnpinruntime.procUnpin
//go:nosplit
funcprocUnpin()
runtime.procPin 和 runtime.procUnpin的實(shí)現(xiàn)代碼在Go 標(biāo)準(zhǔn)庫(kù)下的 src/runtime/proc.go 文件中:
//go:nosplit
funcprocPin()int{
_g_:=getg()
mp:=_g_.m
mp.locks++//鎖定P的調(diào)度
returnint(mp.p.ptr().id)
}
//go:nosplit
funcprocUnpin(){
_g_:=getg()
_g_.m.locks--
}
通過(guò) procPin 函數(shù)鎖定 P 的調(diào)度后再使用 pid,然后通過(guò) procUnpin 釋放 P。不過(guò)這里也需要謹(jǐn)慎使用,使用不當(dāng)會(huì)對(duì)性能產(chǎn)生嚴(yán)重影響。
以上獲取 goid 的方式還有一個(gè)比較大的缺點(diǎn),就是如果 Go 編譯器修改了 g 的結(jié)構(gòu)體,就需要重新適配。
《Go語(yǔ)言高級(jí)編程》第三章第8節(jié) 的實(shí)現(xiàn)可以避免這個(gè)問(wèn)題。其原理是,通過(guò)匯編構(gòu)建一個(gè) g 類(lèi)型的 interface{},然后通過(guò)反射獲取 goid 成員的偏移量。根據(jù)原理,可以如下實(shí)現(xiàn):
funcGetg()int64funcgetgi()interface{}
varg_goid_offsetuintptr=func()uintptr{
g:=getgi()
iff,ok:=reflect.TypeOf(g).FieldByName("goid");ok{
returnf.Offset
}
panic("cannotfindg.goidfield")
}()
TEXT·Getg(SB),NOSPLIT,$0-8
MOVQ(TLS),AX
ADDQ·g_goid_offset(SB),AX
MOVQ(AX),BX
MOVQBX,ret+0(FP)
RET
//funcgetgi()interface{}
TEXT·getgi(SB),NOSPLIT,$32-16
NO_LOCAL_POINTERS
MOVQ$0,ret_type+0(FP)
MOVQ$0,ret_data+8(FP)
GO_RESULTS_INITIALIZED
//getruntime.g
//MOVQ(TLS),AX
MOVQ$0,AX
//getruntime.gtype
MOVQ$type·runtime·g(SB),BX
//MOVQBX,·runtime_g_type(SB)
//returninterface{}
MOVQBX,ret_type+0(FP)
MOVQAX,ret_data+8(FP)
RET
實(shí)際上還可以繼續(xù)簡(jiǎn)化:
varruntime_g_typeuint64//go源碼中聲明
vargGoidOffsetuintptr=func()uintptr{//nolint
varifaceinterface{}
typeefacestruct{
_typeuint64
dataunsafe.Pointer
}
//結(jié)構(gòu)iface后,修改他的類(lèi)型為g
(*eface)(unsafe.Pointer(&iface))._type=runtime_g_type
iff,ok:=reflect.TypeOf(iface).FieldByName("goid");ok{
returnf.Offset
}
panic("cannotfindg.goidfield")
}()
GLOBL·runtime_g_type(SB),NOPTR,$8
DATA·runtime_g_type+0(SB)/8,$type·runtime·g(SB)//匯編中初始化。匯編中可以訪(fǎng)問(wèn) package 的私有變量
5.2、Monkey Patch
Go 語(yǔ)言實(shí)現(xiàn)猴子打點(diǎn)的 package 不一定需要使用匯編,比如 bouk/monkey 和 go-kiss/monkey。不過(guò)字節(jié)開(kāi)源的 monkey 和 內(nèi)部的 mockito 都使用了匯編。他們有一個(gè)同源的依賴(lài)庫(kù),分別在 mockey/internal/monkey 目錄和 mockito/monkey 目錄下。
其 Patch() 的調(diào)用路徑如下:Build() -> Patch() -> PatchValue() -> WriteWithSTW() -> Write() -> do_replace_code() 其中 do_replace_code() 是匯編實(shí)現(xiàn)的,作用是使用 mprotect 系統(tǒng)調(diào)用來(lái)修改內(nèi)存權(quán)限(mprotect系統(tǒng)調(diào)用是修改內(nèi)存頁(yè)屬性的)。原因是:可執(zhí)行代碼區(qū)是只讀的,需要修改為可讀寫(xiě)后才能修改,修改為可執(zhí)行后才能執(zhí)行(有想用 Go 寫(xiě)病毒的,可以參考一下)。
func(builder*MockBuilder)Build()*Mocker{
mocker:=Mocker{target:reflect.ValueOf(builder.target),builder:builder}
mocker.buildHook(builder)
mocker.Patch()
return&mocker
}
func(mocker*Mocker)Patch()*Mocker{
mocker.lock.Lock()
defermocker.lock.Unlock()
ifmocker.isPatched{
returnmocker
}
mocker.patch=monkey.PatchValue(mocker.target,mocker.hook,reflect.ValueOf(mocker.proxy),mocker.builder.unsafe)
mocker.isPatched=true
addToGlobal(mocker)
mocker.outerCaller=tool.OuterCaller()
returnmocker
}
//PatchValuereplacethetargetfunctionwithahookfunction,andstoresthetargetfunctionintheproxyfunction//forfuturerestore.Targetandhookarevaluesoffunction.Proxyisavalueofproxyfunctionpointer.funcPatchValue(target,hook,proxyreflect.Value,unsafebool)*Patch{
tool.Assert(hook.Kind()==reflect.Func,"'%s'isnotafunction",hook.Kind())
tool.Assert(proxy.Kind()==reflect.Ptr,"'%v'isnotafunctionpointer",proxy.Kind())
tool.Assert(hook.Type()==target.Type(),"'%v'and'%s'mismatch",hook.Type(),target.Type())
tool.Assert(proxy.Elem().Type()==target.Type(),"'*%v'and'%s'mismatch",proxy.Elem().Type(),target.Type())
targetAddr:=target.Pointer()
//ThefirstfewbytesofthetargetfunctioncodeconstbufSize=64
targetCodeBuf:=common.BytesOf(targetAddr,bufSize)
//constructthebranchinstruction,i.e.jumptothehookfunction
hookCode:=inst.BranchInto(common.PtrAt(hook))
//searchthecuttingpointofthetargetcode,i.e.theminimumlengthoffullinstructionsthatislongerthanthehookCode
cuttingIdx:=inst.Disassemble(targetCodeBuf,len(hookCode),!unsafe)
//constructtheproxycode
proxyCode:=common.AllocatePage()
//savetheoriginalcodebeforethecuttingpointcopy(proxyCode,targetCodeBuf[:cuttingIdx])
//constructthebranchinstruction,i.e.jumptothecuttingpointcopy(proxyCode[cuttingIdx:],inst.BranchTo(targetAddr+uintptr(cuttingIdx)))
//injecttheproxycodetotheproxyfunction
fn.InjectInto(proxy,proxyCode)
tool.DebugPrintf("PatchValue:hookcodelen(%v),cuttingIdx(%v)
",len(hookCode),cuttingIdx)
//replacetargetfunctioncodesbeforethecuttingpoint
mem.WriteWithSTW(targetAddr,hookCode)
return&Patch{base:targetAddr,code:proxyCode,size:cuttingIdx}
}
//WriteWithSTWcopiesdatabytestothetargetaddressandreplacestheoriginalbytes,duringwhichitwillstopthe//world(onlythecurrentgoroutine'sPisrunning).funcWriteWithSTW(targetuintptr,data[]byte){
common.StopTheWorld()
defercommon.StartTheWorld()
err:=Write(target,data)
tool.Assert(err==nil,err)
}
而 Write 函數(shù)的實(shí)現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux.go,其代碼如下:
packagemem
import(
"syscall""github.com/bytedance/mockey/internal/monkey/common"
)
funcWrite(targetuintptr,data[]byte)error{
do_replace_code(target,common.PtrOf(data),uint64(len(data)),syscall.SYS_MPROTECT,
syscall.PROT_READ|syscall.PROT_WRITE,syscall.PROT_READ|syscall.PROT_EXEC))
returnnil
}
funcdo_replace_code(
_uintptr,//void*addr
_uintptr,//void*data
_uint64,//size_tsize
_uint64,//intmprotect
_uint64,//intprot_rw
_uint64,//intprot_rx
)
do_replace_code 函數(shù)的匯編實(shí)現(xiàn)在 github.com/bytedance/mockey/internal/monkey/mem/write_linux_amd64.s,代碼如下:
#include"textflag.h"
#defineNOP8BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;BYTE$0x90;
#defineNOP64NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;NOP8;
#defineNOP512NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;NOP64;
#defineNOP4096NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;NOP512;
#defineaddrarg+0x00(FP)
#definedataarg+0x08(FP)
#definesizearg+0x10(FP)
#definemprotectarg+0x18(FP)
#defineprot_rwarg+0x20(FP)
#defineprot_rxarg+0x28(FP)
#defineCMOVNEQ_AX_CX
BYTE$0x48
BYTE$0x0f
BYTE$0x45
BYTE$0xc8
TEXT·do_replace_code(SB),NOSPLIT,$0x30-0
JMPSTART
NOP4096
START:
MOVQaddr,DI
MOVQsize,SI
MOVQDI,AX
ANDQ$0x0fff,AX
ANDQ$~0x0fff,DI
ADDQAX,SI
MOVQSI,CX
ANDQ$0x0fff,CX
MOVQ$0x1000,AX
SUBQCX,AX
TESTQCX,CX
CMOVNEQ_AX_CX
ADDQCX,SI
MOVQDI,R8
MOVQSI,R9
MOVQmprotect,AX
MOVQprot_rw,DX
SYSCALL
MOVQaddr,DI
MOVQdata,SI
MOVQsize,CX
REP
MOVSB
MOVQR8,DI
MOVQR9,SI
MOVQmprotect,AX
MOVQprot_rx,DX
SYSCALL
JMPRETURN
NOP4096
RETURN:
RET
5.3、 優(yōu)化獲取行號(hào)性能
筆者另一篇掘金文章 《golang文件行號(hào)探索》 中有詳細(xì)說(shuō)明,代碼如下:
//stack_amd64.gotypeLineuintptrfuncNewLine()Line
varrcuCacheunsafe.Pointer=func()unsafe.Pointer{
m:=make(map[Line]string)
returnunsafe.Pointer(&m)
}()
func(lLine)LineNO()(linestring){
mPCs:=*(*map[Line]string)(atomic.LoadPointer(&rcuCache))
line,ok:=mPCs[l]
if!ok{
file,n:=runtime.FuncForPC(uintptr(l)).FileLine(uintptr(l))
line=file+":"+strconv.Itoa(n)
mPCs2:=make(map[Line]string,len(mPCs)+10)
mPCs2[l]=line
for{
p:=atomic.LoadPointer(&rcuCache)
mPCs=*(*map[Line]string)(p)
fork,v:=rangemPCs{
mPCs2[k]=v
}
swapped:=atomic.CompareAndSwapPointer(&rcuCachep,unsafe.Pointer(&mPCs2))
ifswapped{
break
}
}
}
return
}
#stack_amd64.s
TEXT·NewLine(SB),NOSPLIT,$0-8
MOVQretpc-8(FP),AX
SUBQ$1,AX//注意,這里要-1
MOVQAX,ret+0(FP)
RET
該代碼除了使用匯編獲取行號(hào)外,還是用了無(wú)鎖的 RCU(Read-copy update) 算法提升并發(fā)查詢(xún)速度。還有一點(diǎn)要注意的,retpc-8(FP) 是函數(shù)返回地址,也就是調(diào)用指令 CALL 的下一行指令, 所以需要 -1 才能得到 CALL 指令的 pc,參考Go 源碼 src/runtime/traceback.g 的這段注釋?zhuān)?/p>
//file/lineinformationusingpc-1,becausethatisthepcofthe
//callinstruction(moreprecisely,thelastbyteofthecallinstruction).
//Callersexpectthepcbuffertocontainreturnaddressesanddothe
//same-1themselves,sowekeeppcunchanged.
//Whenthepcisfromasignal(e.g.profilerorsegv)thenwewant
//tolookupfile/lineinformationusingpc,andwestorepc+1inthe
//pcbuffersocallerscanunconditionallysubtract1beforelookingup.
//Seeissue34123.
//Thepccanbeatfunctionentrywhentheframeisinitializedwithout
//actuallyrunningcode,likeruntime.mstart.
5.4、 優(yōu)化獲取調(diào)用棧性能
筆者另一篇掘金文章 《關(guān)于 golang 錯(cuò)誤處理的一些優(yōu)化想法》 中有詳細(xì)說(shuō)明。stack_amd64.go 代碼:
//go:buildamd64//+buildamd64packageerrors
import(
_"unsafe"
)
funcbuildStack(s[]uintptr)int
stack_amd64.s 代碼:
//go:buildamd64||amd64p32||arm64
//+buildamd64amd64p32arm64
#include"go_asm.h"
#include"textflag.h"
#include"funcdata.h"
//funcbuildStack(s[]uintptr)int
TEXT·buildStack(SB),NOSPLIT,$24-8
NO_LOCAL_POINTERS
MOVQcap+16(FP),DX//s.cap
MOVQp+0(FP),AX//s.ptr
MOVQ$0,CX//loop.i
loop:
MOVQ+8(BP),BX//lastpc->BX
SUBQ$1,BX
MOVQBX,0(AX)(CX*8)//s[i]=BX
ADDQ$1,CX//CX++/i++
CMPQCX,DX//ifs.len>=s.cap{return}
JAEreturn//無(wú)符號(hào)大于等于就跳轉(zhuǎn)
MOVQ+0(BP),BP//lastBP;展開(kāi)調(diào)用棧至上一層
CMPQBP,$0//if(BP)<=?0?{?return}
JAloop//無(wú)符號(hào)大于就跳轉(zhuǎn)
return:
MOVQCX,n+24(FP)//retn
RET
5.5、 字符串比較
Go 語(yǔ)言源碼里的字符串比較函數(shù),實(shí)際上使用了 SIMD 指令加速,由匯編實(shí)現(xiàn)。源碼在 Go 源碼文件中:src/cmd/compile/internal/typecheck/builtin/runtime.go :
funccmpstring(string,string)int
src/internal/bytealg/compare_amd64.s:
TEXT·Compare(SB),NOSPLIT,$0-56
//AX=a_base(wantinSI)
//BX=a_len(wantinBX)
//CX=a_cap(unused)
//DI=b_base(wantinDI)
//SI=b_len(wantinDX)
//R8=b_cap(unused)
MOVQSI,DX
MOVQAX,SI
JMPcmpbody<>(SB)
TEXTruntime·cmpstring(SB),NOSPLIT,$0-40
//AX=a_base(wantinSI)
//BX=a_len(wantinBX)
//CX=b_base(wantinDI)
//DI=b_len(wantinDX)
MOVQAX,SI
MOVQDI,DX
MOVQCX,DI
JMPcmpbody<>(SB)
//input:
//SI=a
//DI=b
//BX=alen
//DX=blen
//output:
//AX=output(-1/0/1)
TEXTcmpbody<>(SB),NOSPLIT,$0-0
CMPQSI,DI
...
loop:
CMPQR8,$16
JBE_0through16
MOVOU(SI),X0
MOVOU(DI),X1
PCMPEQBX0,X1
PMOVMSKBX1,AX
XORQ$0xffff,AX//convertEQtoNE
JNEdiff16//branchifatleastonebyteisnotequal
ADDQ$16,SI
ADDQ$16,DI
SUBQ$16,R8
JMPloop
···
這里 MOVOU、PCMPEQB、PMOVMSKB 等就是 SIMD 指令。如果想詳細(xì)了解 SIMD 指令可以看一下 Intel 的官方文檔 《Intel Intrinsics Guide》。另,據(jù)筆者的嘗試,SSE 和 SSE2 指令是可以直接在 Go 語(yǔ)言會(huì)便利使用的。有想法的同學(xué)可以自己驗(yàn)證一下其他 SIMD 指令。
5.6、 字符串搜索
我們常用的字符串搜索函數(shù) strings.Index,也使用了匯編實(shí)現(xiàn)的 SIMD 指令加速。代碼在 Go 源碼文件 src/strings/strings.go 下:
//Indexreturnstheindexofthefirstinstanceofsubstrins,or-1ifsubstrisnotpresentins.funcIndex(s,substrstring)int{
n:=len(substr)
switch{
casen==0:
return0casen==1:
returnIndexByte(s,substr[0])
casen==len(s):
ifsubstr==s{
return0
}
return-1casen>len(s):
return-1casen<=?bytealg.MaxLen:
????????//Usebruteforcewhensandsubstrbotharesmalliflen(s)<=?bytealg.MaxBruteForce?{
returnbytealg.IndexString(s,substr)
...
}
//IndexBytereturnstheindexofthefirstinstanceofcins,or-1ifcisnotpresentins.funcIndexByte(sstring,cbyte)int{
returnbytealg.IndexByteString(s,c)
}
IndexByteString 函數(shù)聲明在 src/internal/bytealg/indexbyte_native.go
//go:build386||amd64||s390x||arm||arm64||ppc64||ppc64le||mips||mipsle||mips64||mips64le||riscv64||wasm
packagebytealg
//go:noescape
funcIndexByte(b[]byte,cbyte)int
//go:noescape
funcIndexByteString(sstring,cbyte)int
src/internal/bytealg/index_native.go
//go:buildamd64||arm64||s390x||ppc64le||ppc64
packagebytealg
//go:noescape
//Indexreturnstheindexofthefirstinstanceofbina,or-1ifbisnotpresentina.
//Requires2<=?len(b)?<=?MaxLen.
funcIndex(a,b[]byte)int
//go:noescape
//IndexStringreturnstheindexofthefirstinstanceofbina,or-1ifbisnotpresentina.
//Requires2<=?len(b)?<=?MaxLen.
funcIndexString(a,bstring)int
匯編實(shí)現(xiàn)在 src/internal/bytealg/indexbyte_amd64.s
#include"go_asm.h"
#include"textflag.h"
TEXT·IndexByte(SB),NOSPLIT,$0-40
MOVQb_base+0(FP),SI
MOVQb_len+8(FP),BX
MOVBc+24(FP),AL
LEAQret+32(FP),R8
JMPindexbytebody<>(SB)
TEXT·IndexByteString(SB),NOSPLIT,$0-32
MOVQs_base+0(FP),SI
MOVQs_len+8(FP),BX
MOVBc+16(FP),AL
LEAQret+24(FP),R8
JMPindexbytebody<>(SB)
//input:
//SI:data
//BX:datalen
//AL:bytesought
//R8:addresstoputresult
TEXTindexbytebody<>(SB),NOSPLIT,$0
//ShuffleX0aroundsothateachbytecontains
//thecharacterwe'relookingfor.
MOVDAX,X0
PUNPCKLBWX0,X0
PUNPCKLBWX0,X0
PSHUFL$0,X0,X0
...
src/internal/bytealg/index_amd64.s
#include"go_asm.h"
#include"textflag.h"
TEXT·Index(SB),NOSPLIT,$0-56
MOVQa_base+0(FP),DI
MOVQa_len+8(FP),DX
MOVQb_base+24(FP),R8
MOVQb_len+32(FP),AX
MOVQDI,R10
LEAQret+48(FP),R11
JMPindexbody<>(SB)
TEXT·IndexString(SB),NOSPLIT,$0-40
MOVQa_base+0(FP),DI
MOVQa_len+8(FP),DX
MOVQb_base+16(FP),R8
MOVQb_len+24(FP),AX
MOVQDI,R10
LEAQret+32(FP),R11
JMPindexbody<>(SB)
//AX:lengthofstring,thatwearesearchingfor
//DX:lengthofstring,inwhichwearesearching
//DI:pointertostring,inwhichwearesearching
//R8:pointertostring,thatwearesearchingfor
//R11:address,wheretoputreturnvalue
//Note:WewantleninDXandAX,becausePCMPESTRIimplicitlyconsumesthem
TEXTindexbody<>(SB),NOSPLIT,$0
...
筆者驗(yàn)證了一下 IndexByte 和自定義通過(guò) for 循環(huán)實(shí)現(xiàn)建的差別:
funcBenchmarkIndexByte(b*testing.B){
b.Run("IndexByte",func(b*testing.B){
b.ReportAllocs()
fori:=0;i0
k:=0for{
j:=strings.IndexByte(str[k:],']')
ifj0{
break
}
n++
k+=j+1
}
_=n
}
b.SetBytes(int64(b.N))
b.StopTimer()
})
b.Run("for",func(b*testing.B){
b.ReportAllocs()
str:=testdata.TwitterJsonOut
fori:=0;i0fori:=0;ilen(str);i++{
ifstr[i]==']'{
n++
}
}
_=n
}
b.SetBytes(int64(b.N))
b.StopTimer()
})
}
結(jié)果如下:
BenchmarkIndexByte/IndexByte
BenchmarkIndexByte/IndexByte-123072980387.5ns/op7929621.19MB/s0B/op0allocs/op
BenchmarkIndexByte/for
BenchmarkIndexByte/for-125166632417ns/op213777.66MB/s0B/op0allocs/op
由結(jié)果可知,SIMD 的加速性能還是挺好的。不過(guò),實(shí)際上如果 strings.IndexByte() 字符串很短 或 所查找的字符在字符串中大量存在的話(huà),性能甚至?xí)?for 循環(huán)慢。這個(gè)可以自行驗(yàn)證一下。
5.7、 自定義SIMD優(yōu)化
如果感興趣,可以照著 Go 編譯器里的匯編抄,慢慢嘗試。github 上也有許多項(xiàng)目可以抄,比如:minio/sha256-simd
5.8、 隨意跳轉(zhuǎn)
這段代碼個(gè)人覺(jué)得很有意思,雖然有缺陷,但不失為一次大膽的嘗試。筆者另一篇掘金文章《關(guān)于 golang 錯(cuò)誤處理的一些優(yōu)化想法》 中有詳細(xì)說(shuō)明,實(shí)現(xiàn)原理類(lèi)似 C 語(yǔ)言的棧溢出攻擊,就是替換函數(shù)的 RET 返回地址。
測(cè)試代碼如下:
funcTestTagTry0(t*testing.T){
deferfunc(){
fmt.Printf("1->")
}()
tag,err1:=NewTag()//當(dāng)tag.Try(err)時(shí),跳轉(zhuǎn)此處并返回err1
fmt.Printf("2->")
iferr1!=nil{
fmt.Printf("3->")
return
}
deferfunc(){
fmt.Printf("4->")//由于的缺陷:這里 debug 下 defer 不內(nèi)聯(lián),會(huì)執(zhí)行;release 下 defer 內(nèi)聯(lián),不會(huì)執(zhí)行
}()
fmt.Printf("5->")
err2:=errors.New("err2")
tag.Try(err2)//這里err2!=nil,則會(huì)跳轉(zhuǎn)到tag創(chuàng)建處的下一行指令執(zhí)行,即fmt.Printf("2->")
fmt.Printf("6->")
return
}
測(cè)試結(jié)果:
#release下defer內(nèi)聯(lián),不會(huì)輸出4
2->5->2->3->1->
#debug下defer不內(nèi)聯(lián),會(huì)輸出4
2->5->2->3->4->1
5.9 調(diào)用其他 package 的私有函數(shù)
通過(guò)過(guò)擺脫 golang 編譯器的一些約束,調(diào)用其他 package 的私有函數(shù)。如這篇文章《How to call private functions (bind to hidden symbols) in GoLang》。
上面 goid 的例子的最后,也講了通過(guò)匯編使用 package 私有的類(lèi)型,即 DATA ·runtime_g_type+0(SB)/8,$type·runtime·g(SB) ,這里不在重復(fù)。
5.10 提高 CGO 調(diào)用的性能
我們知道,CGO 和系統(tǒng)調(diào)用時(shí),Go 語(yǔ)言需要把 goroutine 的調(diào)用棧切換回 g0 調(diào)用棧,并使用 g0 調(diào)用,整個(gè)過(guò)程性能損耗比較大。實(shí)際上,我們可以通過(guò)匯編適配 C 語(yǔ)言的 ABI 來(lái)直接調(diào)用 C 語(yǔ)言的函數(shù),參考 github 下的這個(gè)庫(kù): petermattis/fastcgo。不過(guò),這么做也有很大的局限性,比如導(dǎo)致棧溢出、因 goroutine 無(wú)法被搶占而影響 GC 性能等。
審核編輯 :李倩
-
寄存器
+關(guān)注
關(guān)注
31文章
5599瀏覽量
129568 -
計(jì)數(shù)器
+關(guān)注
關(guān)注
32文章
2307瀏覽量
97824 -
Go
+關(guān)注
關(guān)注
0文章
45瀏覽量
12538
原文標(biāo)題:5、 有哪些有意思的使用場(chǎng)景
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
《微機(jī)原理與匯編語(yǔ)言》微機(jī)基礎(chǔ)知識(shí)
labview基礎(chǔ)知識(shí)
關(guān)于C語(yǔ)言的基礎(chǔ)知識(shí)
ARM匯編基礎(chǔ)知識(shí)點(diǎn)匯總,錯(cuò)過(guò)肯定后悔
匯編指令基礎(chǔ)知識(shí)
通信基礎(chǔ)知識(shí)教程
計(jì)算機(jī)基礎(chǔ)知識(shí)介紹
匯編語(yǔ)言學(xué)習(xí)課件_微處理器基礎(chǔ)知識(shí)
使用Eclipse基礎(chǔ)知識(shí)
《微機(jī)原理與匯編語(yǔ)言》微機(jī)基礎(chǔ)知識(shí)
電源管理基礎(chǔ)知識(shí)電源管理基礎(chǔ)知識(shí)電源管理基礎(chǔ)知識(shí)
匯編基礎(chǔ)知識(shí)教程之?dāng)?shù)據(jù)類(lèi)型與寄存器
匯編基礎(chǔ)知識(shí)教程之ARM匯編簡(jiǎn)介
Go匯編基礎(chǔ)知識(shí)
評(píng)論