參考:?
Go 匯編函數(shù) - Go 語言高級編程
Go 嵌套匯編 - 掘金 (juejin.cn)
前言:
Golang 適用 Go-Runtime(Go 運行時,嵌入在被編譯的PE可執(zhí)行文件之中)來管理調(diào)度協(xié)同程式的運行。
Go 語言沒有多線程(MT)的概念,在 Go 語言之中,每個 Go 協(xié)程就類似開辟了一個新的線程,效率上,肯定是比分配線程好的。
但也僅限于分配協(xié)程,及單個進程可以跑幾萬個乃至幾十萬個協(xié)同程序,這是線程無法比擬的,因為在操作系統(tǒng)之中,最小執(zhí)行單元的單位就是線程了,但是線程相對協(xié)同程序來說,過重,無論是內(nèi)存還是CPU。
但不意味著 Go 協(xié)程執(zhí)行的效率比線程要好,別太自信與盲目,協(xié)程是比不了線程代碼CPU執(zhí)行效率的。
上面也提到了,只是可以同時開辟幾萬個乃至幾十萬個協(xié)程,并且啟動協(xié)程速度比線程快非常多,這是它的優(yōu)勢,但是缺點也很明顯,在物理線程上執(zhí)行 Go 協(xié)同程式的代碼效率不高。
目前世界上最快的協(xié)同程序切換,應(yīng)該是 C/C++ 之中的:
State Threads Library (sourceforge.net)
boost::context?
兩個庫各有千秋,但相對來說 boost 更好用一些,在這里需要提醒大家一點,應(yīng)用程序之中運行協(xié)同程序,它是依托于進程之中的物理線程上執(zhí)行的。
來到正題,我們先來探討 Golang 到底是 “Stackless” 無棧輕量協(xié)程,還是 “Stackful” 有棧重量協(xié)程呢?
那么就有必要分析清楚,有棧協(xié)程跟無棧協(xié)程之間到底有什么區(qū)別。
首先:
1、有棧協(xié)程
? ? ? 1.1、棧協(xié)程是一種基于線程或進程的協(xié)程實現(xiàn)方式。
? ? ? 1.2、棧協(xié)程擁有自己的執(zhí)行棧,可以獨立地管理棧幀、局部變量和函數(shù)調(diào)用。
? ? ? 1.3、棧協(xié)程的切換需要保存和恢復(fù)整個執(zhí)行上下文,包括棧指針、寄存器等。
? ? ? 1.4、由于棧協(xié)程具有獨立的執(zhí)行棧,因此它們可以支持遞歸調(diào)用和深度嵌套。
? ? ? 1.5、由于棧協(xié)程需要額外的資源來維護棧,因此在創(chuàng)建和銷毀方面可能會有一些開銷。?
2、無棧協(xié)程
? ? ? 2.1、無棧協(xié)同是一種基于用戶空間的協(xié)程實現(xiàn)方式。
? ? ? 2.2、無棧協(xié)同沒有獨立的執(zhí)行棧,它們共享相同的調(diào)用棧?!局攸c】
? ? ? 2.3、無棧協(xié)同使用狀態(tài)機來管理協(xié)程的執(zhí)行,并通過保存和恢復(fù)狀態(tài)來實現(xiàn)協(xié)程的切換。
? ? ? 2.4、由于無棧協(xié)同共享調(diào)用棧,因此它們不能支持遞歸調(diào)用和深度嵌套。
? ? ? 2.5、無棧協(xié)同通常比棧協(xié)程更輕量級,創(chuàng)建和銷毀開銷較小。
似乎從上述定義的概念來說,Golang 是有棧協(xié)議?但真的是這樣嗎?顯然不是的,首先真正意義上的有棧協(xié)程,是無法被運行時代管的。
有棧協(xié)程存在以下幾個限制:
1、如果開發(fā)人員切換協(xié)程處理不當(dāng)?shù)那闆r下,會導(dǎo)致協(xié)程棧內(nèi)存泄漏問題。
2、如果開發(fā)人員在多個線程之中執(zhí)行
3、有棧協(xié)程無法動態(tài)擴展計算??臻g,所以有棧協(xié)程需要在分配時,明確指定??臻g大小。
一個協(xié)同程序可以在多個線程上按保證順序性(時序)進行處理,無論是有棧協(xié)同程序、或者是無棧協(xié)同程序,均可以。
Go 協(xié)同程序是屬于 “Stackless” 無棧協(xié)程的類型,但 Go 為了實現(xiàn)協(xié)同程序能像 Stackful 有棧協(xié)程一樣,擁有屬于自己的外掛??臻g,并且支持動態(tài)棧空間擴容。
但要注意一點:
1、Go 協(xié)程可能在不同的線程上面被執(zhí)行,雖然 Go 語言運行時保證了,單一協(xié)同程序執(zhí)行的時序性,但開發(fā)人員需要在其中注意協(xié)同程序之間的同步問題,類似多線程并發(fā)編程。
2、若要實現(xiàn)同步鎖的情況,人們需要考慮多線程問題,否則這可能造成很嚴重的后果,即 Go 運行時附著的工作線程被阻塞,同時最好的實現(xiàn)方式偽同步鎖,如利用管道來實現(xiàn)類似效果。
相對傳統(tǒng)的 TTASLock/CAS自選鎖實現(xiàn),可能不太適合Go 這種結(jié)構(gòu)的程序,這是因為:Go 協(xié)同程序在沒有執(zhí)行異步的情況下是不會讓出線程CPU的,你可以理解為,你需要執(zhí)行類似文件IO、網(wǎng)絡(luò)IO、或者調(diào)用 Go 運行時庫之中的同步庫,例如:sync.Mutex 產(chǎn)生了阻塞行為
鑒于?Go 運行時是多線程執(zhí)行,在不阻塞 Go 運行時最大工作線程的情況下,其它協(xié)程,仍舊是可以正常就緒的工作的,這取決于運行時調(diào)度。
所以嚴格意義上來說,Go 協(xié)程屬于 “Stackless” + “Stackful” 的變種協(xié)程,它屬于 “Stackless” 無棧協(xié)同程序的一種,但 Go 編譯器實現(xiàn)對其用戶代碼進行展開,并分配一個 “Go 外掛計算棧內(nèi)存空間單元”,而非真正意義上的函數(shù)棧,如同C#、C++、C#、ASM、IL函數(shù)的調(diào)用堆棧。
有棧協(xié)程無法放大執(zhí)行堆棧的根本原因是寄存器,EIP、RIP,及地址鏈之間存在上下依賴問題等等,Go 并非是真的有棧協(xié)程,自然不會存在這個問題,它本來就是由編譯器支持的黑魔法,實現(xiàn)的協(xié)同程序(“重點:最終會被展開編譯為狀態(tài)機切換的”),但這類編譯器不能編譯過度復(fù)雜協(xié)同應(yīng)用程序,雖然我個人是相信 Google 的技術(shù)水平的,但并不代表,不對 Stackless 協(xié)程先天存在的對于編譯器的復(fù)雜性,感到一絲憂慮,這個世界上不存在完美的技術(shù),這類編譯器完全內(nèi)部實現(xiàn)的純純黑盒,對開發(fā)人員來說不太容易掌控到更多的細節(jié)。
Go 通過外掛計算??臻g的解決方案,在該 Go ??臻g內(nèi)不保存任何寄存器之類的值,僅存儲調(diào)用函數(shù)棧幀的元RID、參數(shù)、變量等(值或引用),所以在??臻g不足時,進行擴大外掛棧時。
即:分配新的??臻g內(nèi)存,并把原棧內(nèi)存復(fù)制過來,在釋放原棧內(nèi)存空間的內(nèi)存,并把新的棧內(nèi)存首地址(指針)掛載到當(dāng)前 Go 協(xié)同程序的棧頂指針、棧底指針。
在復(fù)制并放大?Go 協(xié)程棧內(nèi)存空間的時候,會導(dǎo)致該協(xié)同被同步阻塞,恢復(fù)取決于這個步驟在何時完成。
Go 棧空間雖然不會保存寄存器的值,但并不意味著 Go 程序不會適用目標(biāo)平臺匯編指令集
下述是一個很簡單的 Go 加法函數(shù),返回參數(shù) x+y 的值:
package main
func Add(x int, y int) int {
return x + y
}
func main() {}
那么 Go 編譯器會輸出以下的匯編指令
TEXT main.Add(SB), NOSPLIT|NOFRAME|ABIInternal, $0-16
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $5, main.Add.arginfo1(SB)
FUNCDATA $6, main.Add.argliveinfo(SB)
PCDATA $3, $1
ADDQ BX, AX
RET
TEXT main.main(SB), NOSPLIT|NOFRAME|ABIInternal, $0-0
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
RET
從上述的代碼中,我們可以清晰的看到,出現(xiàn)了并非X86/X64匯編語法的,F(xiàn)UNCDATA 、PCDATA? 兩個指令。
它們是 GO 匯編之中的偽指令,注意它是偽指令,意思就是說這東西不能用,除了GO的編譯器能理解它之外,其它的匯編器,無論 GCC、VC++ 都是不認識這個東西。
人們可以理解,Go 存在兩個編譯過程,一個前端編譯器,一個后端編譯器,前端編譯器就是把我們寫的 .go 源文件的程序代碼編譯為 Go 后端編譯器認識的 Go 匯編指令集代碼。
這的確很類似于 JAVA/JVM 編譯的字節(jié)碼、C# 編譯器的 MSIL 中間指令代碼,但又存在明顯的區(qū)別,人們可以顯著的參考下述在ARM平臺輸出的 Go 匯編代碼
TEXT main.Add(SB), LEAF|NOFRAME|ABIInternal, $-4-12
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $5, main.Add.arginfo1(SB)
MOVW main.x(FP), R0
MOVW main.y+4(FP), R1
ADD R1, R0, R0
MOVW R0, main.~r0+8(FP)
JMP (R14)
TEXT main.main(SB), LEAF|NOFRAME|ABIInternal, $-4-0
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
JMP (R14)
人們可以明顯的看到,除了幾個偽指令是相同他的,但是內(nèi)部實現(xiàn)所使用的指令發(fā)生了變化,這是因為,Go 每個平臺編譯器生成的 Go 匯編代碼會根據(jù)CPU指令集平臺的不同而不同,這是因為 Go 雖然編譯的是只能給 Go 后端編譯器看的匯編代碼。
但不意味著它會完全按照先編譯為字節(jié)碼、中間代碼的形式,Go 前端編譯器輸出的 Go 匯編,在編譯的過程中,就已經(jīng)按照目的平臺的指令集進行了一部分的翻譯(不完全是真匯編,但匯編已很接近了。)
剩下那部分偽指令是讓 Go 匯編器,在構(gòu)建目的程序時,所需處理的東西,就是GC、外掛??臻g內(nèi)存上面的參數(shù)、局部變量讀取這些實現(xiàn),最后生成的目的匯編代碼,才是用來編譯為目的PE、ELF可執(zhí)行文件的。
OK:這里簡單的描述下上面X86匯編的意義,ARM我不怎么看得懂,所以不在此處獻丑了
第一句 Go 匯編指令:
TEXT ? ?main.Add(SB), NOSPLIT|NOFRAME|ABIInternal, $0-16
1、TEXT: 這是一個偽指令,用于指示下面的代碼是函數(shù)代碼(類似于其他匯編語言中的函數(shù)標(biāo)簽)。
2、main.Add(SB): main.Add 是函數(shù)的名稱,SB 表示 Static Base(靜態(tài)基址),它是一個匯編符號,指示函數(shù)相對于全局數(shù)據(jù)區(qū)的偏移量。
3、NOSPLIT|NOFRAME|ABIInternal: 這是函數(shù)的屬性標(biāo)志。NOSPLIT 指示編譯器不應(yīng)在函數(shù)內(nèi)插入棧分裂代碼,NOFRAME 指示編譯器不應(yīng)創(chuàng)建函數(shù)堆棧幀,ABIInternal 表示該函數(shù)的調(diào)用約定為 Go 內(nèi)部使用。
4、$0-16: 這是函數(shù)的棧幀大小指令。$0 表示該函數(shù)不會在棧上分配任何局部變量的空間,-16 表示函數(shù)會從參數(shù)中讀取16字節(jié)的數(shù)據(jù)。
注意:這個棧空間指的是 Go 程序外掛的棧哈,不是進程線程的棧空間。(或為虛擬棧空間)
第二句 Go 匯編指令:
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
1、這是一個 FUNCDATA 偽指令,用于插入與垃圾回收(garbage collection)相關(guān)的元數(shù)據(jù)。
2、$0 表示這段元數(shù)據(jù)的索引值為 0(參數(shù)位:0 = X)
3、gclocals·g2BeySu+wFnoycgXfElmcg==(SB) 是一個符號名,它引用了一個包含局部變量和參數(shù)信息的數(shù)據(jù)結(jié)構(gòu)。
第三句 Go 匯編指令:
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
跟第二句沒區(qū)別,元數(shù)據(jù)索引值為 1(參數(shù)位:1 = Y)
第四句 Go 匯編指令:
FUNCDATA $5, main.Add.arginfo1(SB)
main.Add.arginfo1(SB) 是獲取 “描述函數(shù)參數(shù)類型和數(shù)量的數(shù)據(jù)結(jié)構(gòu)的引用地址”。
Go 語言沒有顯示的函數(shù)簽名聲明,所以編譯器需要這個函數(shù)的參數(shù)信息,以便于可以正確的傳遞參數(shù)值給該函數(shù)。
第五句?Go 匯編指令:
FUNCDATA $6, main.Add.argliveinfo(SB)
main.Add.argliveinfo(SB) 是獲取 “描述函數(shù)參數(shù)活躍性的數(shù)據(jù)結(jié)構(gòu)的引用地址”
參數(shù)的活躍性指的是在函數(shù)執(zhí)行期間哪些參數(shù)被使用了。這些信息對于優(yōu)化代碼的執(zhí)行效率非常重要,GO GC在用。
第六句 Go 匯編指令
PCDATA $3, $1
把 $1?的值復(fù)制到 $3,AT&T匯編風(fēng)格是:
操作數(shù) 原操作數(shù), 目標(biāo)操作數(shù)
加法實現(xiàn) GO 匯編指令
ADDQ BX, AX
RET
1、AX 和 BX 寄存器用于存儲 x 和 y 的值。
2、之后,通過 ADDQ BX, AX 指令將 y 的值加到 x 上,并將結(jié)果保存在 AX 寄存器中。
3、最后,使用 RET 指令將結(jié)果返回。
總結(jié):
1、Golang 協(xié)程不會保存CPU寄存器的值。
2、Golang 協(xié)程屬于 Stackless 協(xié)程的一種變種。
3、Golang 通過為外掛計算棧內(nèi)存空間,來實現(xiàn)類似有棧協(xié)程的效果。
4、Golang 兩個協(xié)程可能在不同的物理線程上面工作,所以公用數(shù)據(jù)訪問時,須注意同步問題。
5、Golang 協(xié)程在處理異步操作的時,讓出了當(dāng)前協(xié)程占用的線程CPU,協(xié)程處于WAIT狀態(tài)時, 當(dāng)前協(xié)程依賴的外部數(shù)據(jù),可能在外部發(fā)生了改變或者釋放。
? ? ? 所以,該協(xié)程被喚醒之后(resume\awake)理應(yīng)檢查當(dāng)前依賴數(shù)據(jù)的狀態(tài),如:在該協(xié)程處于 Yield 等待狀態(tài)之中時,其它協(xié)程調(diào)用了 Dispose 函數(shù),釋放了 “它(公用數(shù)據(jù))” 持有的全部被托管及非托管資源。
6、Golang 也會適用寄存器優(yōu)化,但這有一些前提,就是簡單的算術(shù)運算,可以被編譯為寄存器優(yōu)化的代碼,這不沖突,只是最終會把值存儲到 “Go” 為每個協(xié)程分配的外掛棧內(nèi)存空間上面。
就像在 MSIL 之中,人們執(zhí)行 stloc.s、ldloc.s、ldarg.s、starg.s 這些指令集一樣,只不過它不像微軟的 .NET CLR 會把這些代碼編譯為近似 C/C++ 編譯器輸出的目標(biāo)平臺匯編代碼,當(dāng)然不管怎么做,這類由GC系統(tǒng)標(biāo)記的語言,都會在最終編譯輸出的匯編代碼之中插入引用技術(shù)管理的實現(xiàn),區(qū)別是在什么地方插入,當(dāng)然這的看GC系統(tǒng)是怎么設(shè)計的,比如鏈式遍歷的GC,就不需要在每個函數(shù)引用資源的地方去做 AddRef、到結(jié)尾做 ReleaseRef 這樣的行為,但缺點就是GC在處理終結(jié)的時候,CPU開銷比較大。文章來源:http://www.zghlxwxcb.cn/news/detail-823485.html
7、Golang 之中托管資源是通過RID間接引用的,即托管資源并非是直接使用指針,這是因為資源或會被GC壓縮或移動碎片整理,當(dāng)然這個時候會導(dǎo)致阻塞問題,即:GC Pinned 問題。文章來源地址http://www.zghlxwxcb.cn/news/detail-823485.html
到了這里,關(guān)于關(guān)于 Go 協(xié)同程序(Coroutines 協(xié)程)、Go 匯編及一些注意事項。的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!