這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上一篇學了記憶體結構基本知識,本文將學習符號(symbol)、語句的含義。我個人喜歡通過例子來學習,所以,我就從src/runtime/asm_amd64.s裡的bytes·Equal入手吧:)
對應代碼
TEXT bytes·Equal(SB),NOSPLIT,$0-49MOVQa_len+8(FP), BXMOVQb_len+32(FP), CXCMPQBX, CXJNEeqretMOVQa+0(FP), SIMOVQb+24(FP), DILEAQret+48(FP), AXJMPruntime·memeqbody(SB)eqret:MOVB$0, ret+48(FP)RET
預備知識
SB (static base)相關知識
以下是Go asm中的介紹
The SB pseudo-register can be thought of as the origin of memory, so the symbol foo(SB) is the name foo as an address in memory. This form is used to name global functions and data. Adding <>to the name, as in foo<>(SB), makes the name visible only in the current source file, like a top-level static declaration in a C file. Adding an offset to the name refers to that offset from the symbol’s address, so foo+4(SB) is four bytes past the start of foo.
大致翻譯一下,例如foo(SB)的符號,就對應了code segement中的地址,全域可見。當添加了 <> 符號後,就變為了當前檔案可見,類似於C檔案的static聲明,還可以通過添加位移量(offset)來訪問其他地址。
指令格式
例子中的TEXT指令就定義了一個叫bytes·Equal的符號(注意是中點號·),接下來就是對應的指令(可以理解成函數體),而最後RET則是返回指令(退出當前stack)。通常情況下,參數大小後跟隨著stack frame的大小,使用減號(-)分割。$0-49意味著這是一個0-byte的棧,並且有49-byte長的參數。NOSPLIT說明,不允許調度器調整stack frame的大小,這就意味著必須人工指定stack frame大小。但為什麼是49個byte?
因為我們可以看看bytes.Equal的定義
func Equal(a, b []byte) bool
a, b 分別為[]byte(不定長的byte slice),而slice的結構是:
type slice struct { array unsafe.Pointer len int lcap int}
unsafe.Pointer 在amd64上是uintptr,即uint64。int在amd64上背後是int64。因此一個slice佔用了3個qword(word=2byte qual=4 即 2x4=8byte 8x8=64bit),即 3x8 = 24byte,然後又有兩個slice做為參數,再加上一個bool byte,因此,這個call stack frame應該有 24x2 ([]byte) + 1 (bool) = 49byte。又因為不需要局部變數,因此定義為0個.
$0-49
函數指令解構
彙編是門“死腦筋”+“瘋狂簡寫”的語言,接下來是對函數語句的解析,一旦理解了以後,語句是很簡單的。
MOVQa_len+8(FP), BX // move qword, 把a slice的長度放入BX寄存器MOVQb_len+32(FP), CX // 把b slice的長度放入CX寄存器CMPQBX, CX // compare qword, 對比BX,CXJNEeqret // jump not equal, 如果不相等就跳轉至標籤eqret(equal ret)MOVQa+0(FP), SI // 把a的指標放入SI寄存器中MOVQb+24(FP), DI // 把b的指標放入DI寄存器中LEAQret+48(FP), AX // load effective address, 將傳回值的記憶體位址放入AX寄存器中JMPruntime·memeqbody(SB) // JUMP, 跳轉至 runtime·memeqbody(SB) 地址空間eqret:MOVB$0, ret+48(FP) // move byte, 將$0 (意思是數字0, 而false = 0)傳入返回的參數中,即兩個slice不相等。RET
這裡出現了兩個新的概念:
位移量定義,例如a_len+8(FP),還記得上一篇中講過,FP是指在低記憶體位上的嗎?因此,這裡就定義了a_len,即a length = +8(FP),相對於FP位移8個byte(記得slice的結構吧),這個正好是a的長度所在的位置。不記得的話,可以參考
FP +------------> b pointer |+ | +-------> b length| | |v | | +-> b capacityLow | | |+----+----+----+-+--+-+--+--+-+-+| | | | | | | |+-+--+-+--+-+--+----+----+----+++ | | | | | | +-> a capacity +-> return value | | | +------> a length | +-----------> a uint64-pointer
每個空格相當於一個byte
另一個概念是label,彙編不同於進階語言,彙編的條件跳轉基本上都是靠label(標籤)實現的,例子中的eqret,就是個label。
AVX
接下來是精華hugeloop
AVX是Intel引以為傲的SIMD指令集,具體介紹在AVX,Go在字元比較中根據CPU的能力分別會使用SSE、AVX、AVX2,這種指令集最佳化就是我們為什麼要寫彙編的原因了。
// 64 bytes at a time using ymm registers (一次就能對比64個byte,64倍效能就問你怕不怕)hugeloop_avx2:CMPQBX, $64 // 對比字元長度JBbigloop_avx2 // 不夠64個位元組就用其他方法。VMOVDQU(SI), Y0// AVX2 專用載入資料的指令,將SI前32個byte載入進Y0寄存器(512bit)VMOVDQU(DI), Y1// ...VMOVDQU32(SI), Y2VMOVDQU32(DI), Y3VPCMPEQBY1, Y0, Y4 // 對比Y0 - Y1,把結果存入Y4中VPCMPEQBY2, Y3, Y5 // 同上VPANDY4, Y5, Y6// AND 操作VPMOVMSKB Y6, DX// MOVE BYTE MASK , 將Y6中的每8個bit做一個掩碼存入DX中(簡單點就是相同就都是0xf)ADDQ$64, SI// SI位移64個byteADDQ$64, DI// DI位移64個byteSUBQ$64, BX// BX 長度減64CMPLDX, $0xffffffff// 對比DX的低位JEQhugeloop_avx2// 相同則繼續對比VZEROUPPER// 清空Y寄存器MOVB$0, (AX)// 發現不同,返回RET
小結
現在對Golang的彙編比較熟悉了,下一篇會摘抄並翻譯一些注意事項。