golang彙編語言基礎

在深入閱讀runtime和標準庫的源碼時候,發現底層有大片代碼都會與彙編打交道,所以這篇文章主要是介紹golang使用到的彙編。

go彙編語言是一個不可忽視的技術。因爲哪怕只懂一點點彙編,也便於更好地理解計算機原理,也更容易理解Go語言中動態棧/接口等高級特性的實現原理。

本文涉及到計算機架構體系相關的情況時,請假設我們是運行在 linux/amd64 平臺上。

僞彙編

Go 編譯器會輸出一種抽象可移植的彙編代碼,這種彙編並不對應某種真實的硬件架構。之後 Go 的彙編器使用這種僞彙編,爲目標硬件生成具體的機器指令。

僞彙編這一個額外層可以帶來很多好處,最主要的一點是方便將 Go 移植到新的架構上。相關的信息可以參考文後列出的 Rob Pike 的 The Design of the Go Assembler。

go 彙編語言的一個簡單實例

思考下面這行代碼:

//go:noinline
func add(a, b int32) (int32, bool) { 
	return a + b, true 
}

func main() { add(10, 32) }

注意這裏的 //go:noinline 編譯器指令。不要省略掉這部分

將這段代碼編譯到彙編:

"".add STEXT nosplit size=20 args=0x10 locals=0x0
	0x0000 00000 (test1.go:5)	TEXT	"".add(SB), NOSPLIT|ABIInternal, $0-16
	0x0000 00000 (test1.go:5)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:5)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:5)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:6)	PCDATA	$0, $0
	0x0000 00000 (test1.go:6)	PCDATA	$1, $0
	0x0000 00000 (test1.go:6)	MOVL	"".b+12(SP), AX
	0x0004 00004 (test1.go:6)	MOVL	"".a+8(SP), CX
	0x0008 00008 (test1.go:6)	ADDL	CX, AX
	0x000a 00010 (test1.go:6)	MOVL	AX, "".~r2+16(SP)
	0x000e 00014 (test1.go:6)	MOVB	$1, "".~r3+20(SP)
	0x0013 00019 (test1.go:6)	RET
	0x0000 8b 44 24 0c 8b 4c 24 08 01 c8 89 44 24 10 c6 44  .D$..L$....D$..D
	0x0010 24 14 01 c3                                      $...
"".main STEXT size=65 args=0x0 locals=0x18
	0x0000 00000 (test1.go:9)	TEXT	"".main(SB), ABIInternal, $24-0
	0x0000 00000 (test1.go:9)	MOVQ	(TLS), CX
	0x0009 00009 (test1.go:9)	CMPQ	SP, 16(CX)
	0x000d 00013 (test1.go:9)	JLS	58
	0x000f 00015 (test1.go:9)	SUBQ	$24, SP
	0x0013 00019 (test1.go:9)	MOVQ	BP, 16(SP)
	0x0018 00024 (test1.go:9)	LEAQ	16(SP), BP
	0x001d 00029 (test1.go:9)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:9)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:9)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:10)	PCDATA	$0, $0
	0x001d 00029 (test1.go:10)	PCDATA	$1, $0
	0x001d 00029 (test1.go:10)	MOVQ	$137438953482, AX
	0x0027 00039 (test1.go:10)	MOVQ	AX, (SP)
	0x002b 00043 (test1.go:10)	CALL	"".add(SB)
	0x0030 00048 (test1.go:11)	MOVQ	16(SP), BP
	0x0035 00053 (test1.go:11)	ADDQ	$24, SP
	0x0039 00057 (test1.go:11)	RET
	0x003a 00058 (test1.go:11)	NOP
	0x003a 00058 (test1.go:9)	PCDATA	$1, $-1
	0x003a 00058 (test1.go:9)	PCDATA	$0, $-1
	0x003a 00058 (test1.go:9)	CALL	runtime.morestack_noctxt(SB)
	0x003f 00063 (test1.go:9)	JMP	0
	0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2b 48  dH..%....H;a.v+H
	0x0010 83 ec 18 48 89 6c 24 10 48 8d 6c 24 10 48 b8 0a  ...H.l$.H.l$.H..
	0x0020 00 00 00 20 00 00 00 48 89 04 24 e8 00 00 00 00  ... ...H..$.....
	0x0030 48 8b 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00 eb  H.l$.H..........
	0x0040 bf                                               .
	rel 5+4 t=16 TLS+0
	rel 44+4 t=8 "".add+0
	rel 59+4 t=8 runtime.morestack_noctxt+0

接下來一行一行地對這兩個函數進行解析來幫助我們理解編譯器在編譯期間都做了什麼事情。

函數 add

0x0000 00000 (test1.go:5) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16

  1. 0x0000: 當前指令相對於當前函數的偏移量。
  2. TEXT “”.add: TEXT 指令聲明瞭 “”.add 是 .text 段(程序代碼在運行期會放在內存的 .text 段中)的一部分,並表明跟在這個聲明後的是函數的函數體。在鏈接期,"" 這個空字符會被替換爲當前的包名: 也就是說,"".add 在鏈接到二進制文件後會變成 main.add。
  3. (SB): SB 是一個虛擬寄存器,保存了靜態基地址(static-base) 指針,即我們程序地址空間的開始地址。"".add(SB) 表明我們的符號位於某個固定的相對地址空間起始處的偏移位置 (最終是由鏈接器計算得到的)。換句話來講,它有一個直接的絕對地址: 是一個全局的函數符號。

objdump 這個工具能幫我們確認上面這些結論:

 ytlou@ytlou-mac ~/Desktop/golang/golang_study/study/basic/assembly $ objdump -j .text -t test1 | grep 'main.add'
00000000010512e0 l     F __TEXT,__text  main.add
  1. NOSPLIT: 向編譯器表明不應該插入 stack-split 的用來檢查棧需要擴張的前導指令。
    在我們 add 函數的這種情況下,編譯器自己幫我們插入了這個標記: 它足夠聰明地意識到,由於 add 沒有任何局部變量且沒有它自己的棧幀,所以一定不會超出當前的棧;因此每次調用函數時在這裏執行棧檢查就是完全浪費 CPU 循環了。
  2. $0-16: $0 代表即將分配的棧幀大小;而 $16 指定了調用方傳入的參數大小。

Go 的調用規約要求每一個參數都通過棧來傳遞,這部分空間由 caller 在其棧幀(stack frame)上提供。

調用其它函數之前,caller 就需要按照參數和返回變量的大小來對應地增長(返回後收縮)棧。

Go 編譯器沒有 PUSH/POP 族的指令: 棧的增長和收縮是通過在棧指針寄存器 SP 上分別執行減法和加法指令來實現的

與大多數最近的編譯器做法一樣,Go 工具鏈總是在其生成的代碼中,使用相對棧指針(stack-pointer)的偏移量來引用參數和局部變量。這樣使得我們可以用幀指針(frame-pointer)來作爲一個額外的通用寄存器,這一點即使是在那些寄存器數量較少的平臺上也是一樣的(例如 x86)。

“”.b+12(SP) 和 “”.a+8(SP) 分別指向棧的低 12 字節和低 8 字節位置(記住: 棧是向低位地址方向增長的!)。

.a 和 .b 是分配給引用地址的任意別名;儘管 它們沒有任何語義上的含義 ,但在使用虛擬寄存器和相對地址時,這種別名是需要強制使用的。

最後,有兩個重點需要指出:

  1. 第一個變量 a 的地址並不是 0(SP),而是在 8(SP);這是因爲調用方通過使用 CALL 僞指令,把其返回地址保存在了 0(SP) 位置。
  2. 參數是反序傳入的;也就是說,第一個參數和棧頂距離最近。
0x0008 00008 (test1.go:6)	ADDL	CX, AX
0x000a 00010 (test1.go:6)	MOVL	AX, "".~r2+16(SP)
0x000e 00014 (test1.go:6)	MOVB	$1, "".~r3+20(SP)

ADDL 進行實際的加法操作,L 這裏代表 Long,4 字節的值,其將保存在 AX 和 CX 寄存器中的值進行相加,然後再保存進 AX 寄存器中。
這個結果之後被移動到 "".~r2+16(SP)地址處,這是之前調用方專門爲返回值預留的棧空間。這一次 “”.~r2 同樣沒什麼語義上的含義。

stacks 和 Splits

Stacks

由於 Go 程序中的 goroutine 數目是不可確定的,並且實際場景可能會有百萬級別的 goroutine,runtime 必須使用保守的思路來給 goroutine 分配空間以避免喫掉所有的可用內存。

也由於此,每個新的 goroutine 會被 runtime 分配初始爲 2KB 大小的棧空間(Go 的棧在底層實際上是分配在堆空間上的)。

隨着一個 goroutine 進行自己的工作,可能會超出最初分配的棧空間限制(就是棧溢出的意思)。爲了防止這種情況發生,runtime 確保 goroutine 在超出棧範圍時,會創建一個相當於原來兩倍大小的新棧,並將原來棧的上下文拷貝到新棧上。這個過程被稱爲 棧分裂(stack-split),這樣使得 goroutine 棧能夠動態調整大小。

Splits

爲了使棧分裂正常工作,編譯器會在每一個函數的開頭和結束位置插入指令來防止 goroutine 爆棧。

像我們本章早些看到的一樣,爲了避免不必要的開銷,一定不會爆棧的函數會被標記上 NOSPLIT 來提示編譯器不要在這些函數的開頭和結束部分插入這些檢查指令。

基本指令

基礎相關指令可以參考:
x86彙編指令集大全(帶註釋)

寄存器

通用寄存器

應用代碼層面會用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 這 14 個寄存器,雖然 rbp 和 rsp 也可以用,不過 bp 和 sp 會被用來管理棧頂和棧底,最好不要拿來進行運算。

plan9 中使用寄存器不需要帶 r 或 e 的前綴,例如 rax,只要寫 AX 即可:

僞寄存器

Go 的彙編還引入了 4 個僞寄存器,援引官方文檔的描述:

  1. FP: Frame pointer: arguments and locals.
  2. PC: Program counter: jumps and branches.
  3. SB: Static base pointer: global symbols.
  4. SP: Stack pointer: top of stack.

官方的描述稍微有一些問題,我們對這些說明進行一點擴充:

  1. FP: 使用形如 symbol+offset(FP) 的方式,引用函數的輸入參數。例如 arg0+0(FP),arg1+8(FP),使用 FP 不加 symbol 時,無法通過編譯,在彙編層面來講,symbol 並沒有什麼用,加 symbol 主要是爲了提升代碼可讀性。另外,官方文檔雖然將僞寄存器 FP 稱之爲 frame pointer,實際上它根本不是 frame pointer,按照傳統的 x86 的習慣來講,frame pointer 是指向整個 stack frame 底部的 BP 寄存器。假如當前的 callee 函數是 add,在 add 的代碼中引用 FP,該 FP 指向的位置不在 callee 的 stack frame 之內,而是在 caller 的 stack frame 上。具體可參見之後的 棧結構 一章。
  2. PC: 實際上就是在體系結構的知識中常見的 pc 寄存器,在 x86 平臺下對應 ip 寄存器,amd64 上則是 rip。除了個別跳轉之外,手寫 plan9 代碼與 PC 寄存器打交道的情況較少。
  3. SB: 全局靜態基指針,一般用來聲明函數或全局變量,在之後的函數知識和示例部分會看到具體用法。
  4. plan9 的這個 SP 寄存器指向當前棧幀的局部變量的開始位置,使用形如 symbol+offset(SP) 的方式,引用函數的局部變量。offset 的合法取值是 [-framesize, 0),注意是個左閉右開的區間。假如局部變量都是 8 字節,那麼第一個局部變量就可以用 localvar0-8(SP) 來表示。這也是一個詞不表意的寄存器。與硬件寄存器 SP 是兩個不同的東西,在棧幀 size 爲 0 的情況下,僞寄存器 SP 和硬件寄存器 SP 指向同一位置。手寫彙編代碼時,如果是 symbol+offset(SP) 形式,則表示僞寄存器 SP。如果是 offset(SP) 則表示硬件寄存器 SP。務必注意。對於編譯輸出(go tool compile -S / go tool objdump)的代碼來講,目前所有的 SP 都是硬件寄存器 SP,無論是否帶 symbol。

我們這裏對容易混淆的幾點簡單進行說明:

  1. 僞 SP 和硬件 SP 不是一回事,在手寫代碼時,僞 SP 和硬件 SP 的區分方法是看該 SP 前是否有 symbol。如果有 symbol,那麼即爲僞寄存器,如果沒有,那麼說明是硬件 SP 寄存器。
  2. SP 和 FP 的相對位置是會變的,所以不應該嘗試用僞 SP 寄存器去找那些用 FP + offset 來引用的值,例如函數的入參和返回值。
  3. 官方文檔中說的僞 SP 指向 stack 的 top,是有問題的。其指向的局部變量位置實際上是整個棧的棧底(除 caller BP 之外),所以說 bottom 更合適一些。
  4. 在 go tool objdump/go tool compile -S 輸出的代碼中,是沒有僞 SP 和 FP 寄存器的,我們上面說的區分僞 SP 和硬件 SP 寄存器的方法,對於上述兩個命令的輸出結果是沒法使用的。在編譯和反彙編的結果中,只有真實的 SP 寄存器。
  5. FP 和 Go 的官方源代碼裏的 frame pointer 不是一回事,源代碼裏的 frame pointer 指的是 caller BP 寄存器的值,在這裏和 caller 的僞 SP 是值是相等的。

棧結構

					   -----------------                                           
                       current func arg0                                           
                       ----------------- <----------- FP(pseudo FP)                
                        caller ret addr                                            
                       +---------------+                                           
                       | caller BP(*)  |                                           
                       ----------------- <----------- SP(pseudo SP,實際上是當前棧幀的 BP 位置)
                       |   Local Var0  |                                           
                       -----------------                                           
                       |   Local Var1  |                                           
                       -----------------                                           
                       |   Local Var2  |                                           
                       -----------------                -                          
                       |   ........    |                                           
                       -----------------                                           
                       |   Local VarN  |                                           
                       -----------------                                           
                       |               |                                           
                       |               |                                           
                       |  temporarily  |                                           
                       |  unused space |                                           
                       |               |                                           
                       |               |                                           
                       -----------------                                           
                       |  call retn    |                                           
                       -----------------                                           
                       |  call ret(n-1)|                                           
                       -----------------                                           
                       |  ..........   |                                           
                       -----------------                                           
                       |  call ret1    |                                           
                       -----------------                                           
                       |  call argn    |                                           
                       -----------------                                           
                       |   .....       |                                           
                       -----------------                                           
                       |  call arg3    |                                           
                       -----------------                                           
                       |  call arg2    |                                           
                       |---------------|                                           
                       |  call arg1    |                                           
                       -----------------   <------------  hardware SP 位置           
                       | return addr   |                                           
                       +---------------+       

圖上的 caller BP,指的是 caller 的 BP 寄存器值,有些人把 caller BP 叫作 caller 的 frame pointer,實際上這個習慣是從 x86 架構沿襲來的。Go 的 asm 文檔中把僞寄存器 FP 也稱爲 frame pointer,但是這兩個 frame pointer 根本不是一回事。

此外需要注意的是,caller BP 是在編譯期由編譯器插入的,用戶手寫代碼時,計算 frame size 時是不包括這個 caller BP 部分的。是否插入 caller BP 的主要判斷依據是:

  1. 函數的棧幀大小大於 0
  2. 下述函數返回 true
func Framepointer_enabled(goos, goarch string) bool {
    return framepointer_enabled != 0 && goarch == "amd64" && goos != "nacl"
}

如果編譯器在最終的彙編結果中沒有插入 caller BP(源代碼中所稱的 frame pointer)的情況下,僞 SP 和僞 FP 之間只有 8 個字節的 caller 的 return address,而插入了 BP 的話,就會多出額外的 8 字節。也就說僞 SP 和僞 FP 的相對位置是不固定的,有可能是間隔 8 個字節,也有可能間隔 16 個字節。並且判斷依據會根據平臺和 Go 的版本有所不同。

圖上可以看到,FP 僞寄存器指向函數的傳入參數的開始位置,因爲棧是朝低地址方向增長,爲了通過寄存器引用參數時方便,所以參數的擺放方向和棧的增長方向是相反的,即:

                              FP
high ----------------------> low
argN, ... arg3, arg2, arg1, arg0

假設所有參數均爲 8 字節,這樣我們就可以用 symname+0(FP) 訪問第一個 參數,symname+8(FP) 訪問第二個參數,以此類推。用僞 SP 來引用局部變量,原理上來講差不多,不過因爲僞 SP 指向的是局部變量的底部,所以 symname-8(SP) 表示的是第一個局部變量,symname-16(SP)表示第二個,以此類推。當然,這裏假設局部變量都佔用 8 個字節。

圖的最上部的 caller return address 和 current func arg0 都是由 caller 來分配空間的。不算在當前的棧幀內。

因爲官方文檔本身較模糊,我們來一個函數調用的全景圖,來看一下這些真假 SP/FP/BP 到底是個什麼關係:

                                                                                                                              
                                       caller                                                                                 
                                 +------------------+                                                                         
                                 |                  |                                                                         
       +---------------------->  --------------------                                                                         
       |                         |                  |                                                                         
       |                         | caller parent BP |                                                                         
       |           BP(pseudo SP) --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   Local Var0     |                                                                         
       |                         --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   .......        |                                                                         
       |                         --------------------                                                                         
       |                         |                  |                                                                         
       |                         |   Local VarN     |                                                                         
                                 --------------------                                                                         
 caller stack frame              |                  |                                                                         
                                 |   callee arg2    |                                                                         
       |                         |------------------|                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg1    |                                                                         
       |                         |------------------|                                                                         
       |                         |                  |                                                                         
       |                         |   callee arg0    |                                                                         
       |                         ----------------------------------------------+   FP(virtual register)                       
       |                         |                  |                          |                                              
       |                         |   return addr    |  parent return address   |                                              
       +---------------------->  +------------------+---------------------------    <-------------------------------+         
                                                    |  caller BP               |                                    |         
                                                    |  (caller frame pointer)  |                                    |         
                                     BP(pseudo SP)  ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |     Local Var0           |                                    |         
                                                    ----------------------------                                    |         
                                                    |                          |                                              
                                                    |     Local Var1           |                                              
                                                    ----------------------------                            callee stack frame
                                                    |                          |                                              
                                                    |       .....              |                                              
                                                    ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |     Local VarN           |                                    |         
                                  SP(Real Register) ----------------------------                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    |                          |                                    |         
                                                    +--------------------------+    <-------------------------------+         
                                                                                                                              
                                                              callee

argsize 和 framesize 計算規則

argsize

在函數聲明中:

 TEXT pkgname·add(SB),NOSPLIT,$16-32

前面已經說過 $16-32 表示 $framesize-argsize。Go 在函數調用時,參數和返回值都需要由 caller 在其棧幀上備好空間。callee 在聲明時仍然需要知道這個 argsize。argsize 的計算方法是,參數大小求和+返回值大小求和,例如入參是 3 個 int64 類型,返回值是 1 個 int64 類型,那麼這裏的 argsize = sizeof(int64) * 4。

不過真實世界永遠沒有我們假設的這麼美好,函數參數往往混合了多種類型,還需要考慮內存對齊問題。

如果不確定自己的函數簽名需要多大的 argsize,可以通過簡單實現一個相同簽名的空函數,然後 go tool objdump 來逆向查找應該分配多少空間。

framesize

函數的 framesize 就稍微複雜一些了,手寫代碼的 framesize 不需要考慮由編譯器插入的 caller BP,要考慮:

  1. 局部變量,及其每個變量的 size。
  2. 在函數中是否有對其它函數調用時,如果有的話,調用時需要將 callee 的參數、返回值考慮在內。雖然 return address(rip)的值也是存儲在 caller 的 stack frame 上的,但是這個過程是由 CALL 指令和 RET 指令完成 PC 寄存器的保存和恢復的,在手寫彙編時,同樣也是不需要考慮這個 PC 寄存器在棧上所需佔用的 8 個字節的。
  3. 原則上來說,調用函數時只要不把局部變量覆蓋掉就可以了。稍微多分配幾個字節的 framesize 也不會死。
  4. 在確保邏輯沒有問題的前提下,你願意覆蓋局部變量也沒有問題。只要保證進入和退出彙編函數時的 caller 和 callee 能正確拿到返回值就可以。

自己實現彙編函數實例

math.go:

package main

import "fmt"

func add(a, b int) int // 彙編函數聲明

func sub(a, b int) int // 彙編函數聲明

func mul(a, b int) int // 彙編函數聲明

func main() {
	fmt.Println(add(10, 11))
	fmt.Println(sub(99, 15))
	fmt.Println(mul(11, 12))
}

math.s:

#include "textflag.h" // 因爲我們聲明函數用到了 NOSPLIT 這樣的 flag,所以需要將 textflag.h 包含進來

// func add(a, b int) int
TEXT ·add(SB), NOSPLIT, $0-24
    MOVQ a+0(FP), AX // 參數 a
    MOVQ b+8(FP), BX // 參數 b
    ADDQ BX, AX    // AX += BX
    MOVQ AX, ret+16(FP) // 返回
    RET

// func sub(a, b int) int
TEXT ·sub(SB), NOSPLIT, $0-24
    MOVQ a+0(FP), AX
    MOVQ b+8(FP), BX
    SUBQ BX, AX    // AX -= BX
    MOVQ AX, ret+16(FP)
    RET

// func mul(a, b int) int
TEXT ·mul(SB), NOSPLIT, $0-24
    MOVQ  a+0(FP), AX
    MOVQ  b+8(FP), BX
    IMULQ BX, AX    // AX *= BX
    MOVQ  AX, ret+16(FP)
    RET
    // 最後一行的空行是必須的,否則可能報 unexpected EOF

把這兩個文件放在一個目錄下就可以執行 go build 了。

benchmark測試用例:

package main

import "testing"

var Result int

func BenchmarkAddNative(b *testing.B) {
	var r int
	for i := 0; i < b.N; i++ {
		r = int(i) + int(i)
	}
	Result = r
}

func BenchmarkAddAsm(b *testing.B) {
	var r int
	for i := 0; i < b.N; i++ {
		r = add(int(i), int(i))
	}
	Result = r
}

我們看一下benchmark 的執行結果:

~/Desktop/golang/golang_study/study/basic/assembly/math $ go test -bench=. .                   
goos: darwin
goarch: amd64
pkg: study_golang/study/basic/assembly/math
BenchmarkAddNative-12           1000000000               0.256 ns/op
BenchmarkAddAsm-12              741027513                1.58 ns/op
PASS
ok      study_golang/study/basic/assembly/math  1.630s

我們可以看到go原生的自加其實比使用匯編寫的代碼要快的多,這是因爲 Go 現在還不支持彙編函數內聯,所以調用匯編函數執行自加會有一些函數調用的性能損耗,所以自加彙編函數實現有更高的負載。

如果我將BenchmarkAddNative禁用內聯,並將自加單獨抽出來:

package main

import "testing"

var Result int

func addf(a, b int) int{
	return a+b
}

func BenchmarkAddNative(b *testing.B) {
	var r int
	for i := 0; i < b.N; i++ {
		r = addf(i,i)
	}
	Result = r
}

func BenchmarkAddAsm(b *testing.B) {
	var r int
	for i := 0; i < b.N; i++ {
		r = add(int(i), int(i))
	}
	Result = r
}

執行結果:

~/Desktop/golang/golang_study/study/basic/assembly/math $ go test -gcflags=-l -bench=. 
goos: darwin
goarch: amd64
pkg: study_golang/study/basic/assembly/math
BenchmarkAddNative-12           726956421                1.58 ns/op
BenchmarkAddAsm-12              753173751                1.61 ns/op
PASS
ok      study_golang/study/basic/assembly/math  2.694s

在go1.13中,有一些包的裏面的函數被定義爲 原生函數,這些函數會在編譯時候被替代 成彙編代碼,而不是以彙編函數的方式調用。比如:

math/bits
sync/atomic

go atomic.add 原子實現

這裏以 atomic.AddInt64 爲例,調用鏈路是這樣的。

sync/atomic/doc.go

func AddInt64(addr *int64, delta int64) (new int64)

sync/atomic/asm.s

TEXT ·AddInt64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadd64(SB)

runtime∕internal/atomic/asm_amd64.s

TEXT runtime∕internal∕atomic·Xadd64(SB), NOSPLIT, $0-24
	MOVQ	ptr+0(FP), BX   //拿到變量的地址
	MOVQ	delta+8(FP), AX  //拿到增量
	MOVQ	AX, CX // 將增量保存在CX
	LOCK // 鎖總線,多CPU排他執行指令。
	XADDQ	AX, 0(BX) // 將變量原有的值賦值給AX,增量賦值給變量地址指向的值,然後求和保存到變量地址指向的地址。
	ADDQ	CX, AX // 重新計算add之後的值保存到AX用於 return.
	MOVQ	AX, ret+16(FP)
	RET

這裏先解釋一下兩個彙編指令:

  1. LOCK:是一個指令前綴,其後必須跟一條“讀-改-寫”的指令,比如XADDQ、XCHG、CMPXCHG等。 這條指令表明封鎖總線,對CPU緩存的訪問將是排他的。
  2. XADDQ:先交換再累加.( 結果在第二個操作數裏 )

參考文獻:
Golang同步機制的實現
x86彙編指令集大全(帶註釋)
Go 系列文章3 :plan9 彙編入門
Go compiler intrinsics

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章