從Go匯編角度解讀for循環(huán)的問(wèn)題
Go常用的遍歷方式有兩種:for和for-range。實(shí)際上,for-range也只是for的語(yǔ)法糖,本文試圖從匯編代碼入手解釋for循環(huán)是如何工作的。
問(wèn)題
首先來(lái)看看幾個(gè)令人迷惑的地方。
問(wèn)題1:遍歷過(guò)程中取值
func main() { arr := [5]int{1, 2, 3, 4, 5} for _, v := range arr { println(&v) } }
上面這段代碼里,會(huì)打印出什么?
問(wèn)題2:遍歷過(guò)程中修改
arr := []int{1, 2, 3, 4, 5} for v := range arr { arr = append(arr, v) }
上面這段代碼里,遍歷前后arr有哪些變化?
窺探虛實(shí)
對(duì)于問(wèn)題1,我們期待會(huì)打印出5個(gè)不同的地址,實(shí)際上最終打印出來(lái)的都是同一個(gè)地址,我們可以猜測(cè)v在循環(huán)過(guò)程中只聲明了一次??纯磫?wèn)題1的匯編代碼:
0x0028 00040 (main.go:4) MOVQ ""..stmp_0(SB), AX 0x002f 00047 (main.go:4) MOVQ AX, "".arr+24(SP) 0x0034 00052 (main.go:4) MOVUPS ""..stmp_0+8(SB), X0 0x003b 00059 (main.go:4) MOVUPS X0, "".arr+32(SP) 0x0040 00064 (main.go:4) MOVUPS ""..stmp_0+24(SB), X0 0x0047 00071 (main.go:4) MOVUPS X0, "".arr+48(SP) 0x004c 00076 (main.go:5) MOVQ "".arr+24(SP), AX 0x0051 00081 (main.go:5) MOVQ AX, ""..autotmp_2+64(SP) 0x0056 00086 (main.go:5) MOVUPS "".arr+32(SP), X0 0x005b 00091 (main.go:5) MOVUPS X0, ""..autotmp_2+72(SP) 0x0060 00096 (main.go:5) MOVUPS "".arr+48(SP), X0 0x0065 00101 (main.go:5) MOVUPS X0, ""..autotmp_2+88(SP) 0x006a 00106 (main.go:5) XORL AX, AX 0x006c 00108 (main.go:5) JMP 162 0x006e 00110 (main.go:5) MOVQ AX, ""..autotmp_7+16(SP) 0x0073 00115 (main.go:5) MOVQ ""..autotmp_2+64(SP)(AX*8), CX 0x0078 00120 (main.go:5) MOVQ CX, "".v+8(SP) 0x007d 00125 (main.go:6) CALL runtime.printlock(SB) 0x0082 00130 (main.go:6) LEAQ "".v+8(SP), AX 0x0087 00135 (main.go:6) MOVQ AX, (SP) 0x008b 00139 (main.go:6) CALL runtime.printpointer(SB) 0x0090 00144 (main.go:6) CALL runtime.printnl(SB) 0x0095 00149 (main.go:6) CALL runtime.printunlock(SB) 0x009a 00154 (main.go:5) MOVQ ""..autotmp_7+16(SP), AX 0x009f 00159 (main.go:5) INCQ AX 0x00a2 00162 (main.go:5) CMPQ AX, $5 0x00a6 00166 (main.go:5) JLT 110
00040行:MOVQ ""..stmp_0(SB), AX將stmp_0變量里的內(nèi)容放到AX寄存器里,stmp_0實(shí)際上就是arr數(shù)組,在生成的匯編代碼里:
""..stmp_0 SRODATA size=40 0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 0x0020 05 00 00 00 00 00 00 00
由此可以看到stmp_0正是arr數(shù)組。
00106行:XORL AX AX是初始化AX寄存器,AX寄存器里包含當(dāng)前循環(huán)位置。 00108行:JMP 162表示跳轉(zhuǎn)到00162行。 00162行:CMPQ AX $5比較寄存器AX和5,偽代碼:i < 5,如果滿足條件,則跳轉(zhuǎn)到00110行。
00110行00159行為循環(huán)體代碼,注意到00159行INCQ AX, 意即AX寄存器值自增,到這里我們可以大致分析出來(lái)for-range在匯編層面的偽代碼:
for i := 0; i < 5; i++ { }
這也就驗(yàn)證了上面說(shuō)的for-range只是普通for的語(yǔ)法糖。
00110到00120行是循環(huán)體代碼的前半部分。從Go 匯編文檔上看:SP寄存器指向當(dāng)前棧幀的局部變量的開(kāi)始位置,也就是說(shuō)局部變量放在了SP寄存器的棧幀里。
00115行:MOVQ ""..autotmp_2+64(SP)(AX*8), CX,autotmp_*是為臨時(shí)變量自動(dòng)生成的名字,這行匯編做的事情是將某個(gè)v值(注意,是值)放在CX寄存器里。
00120行:MOVQ CX, "".v+8(SP)將CX寄存器里的內(nèi)容放在SP寄存器指向的位置,00125行代碼是一個(gè)隔斷,00125之后的代碼與println有關(guān)。重點(diǎn)在這行代碼,每次循環(huán)都會(huì)將值放在"".v+8(SP)這個(gè)位置,在這個(gè)循環(huán)體代碼里,我們并沒(méi)有看到其他的臨時(shí)變量聲明,到這里,我們可以總結(jié)出:"".v+8(SP)這個(gè)位置就是變量v在棧幀中的位置,由于位置一直沒(méi)有發(fā)生變化,在進(jìn)行&v操作時(shí)取到的會(huì)是同一個(gè)地址。
對(duì)于問(wèn)題1,根據(jù)匯編代碼的分析,我們得出結(jié)論:v在循環(huán)過(guò)程中只會(huì)聲明一次,每次循環(huán)只是將v值替換,并未重新聲明臨時(shí)變量,這樣解釋了問(wèn)題1代碼的輸出結(jié)果。
再回到問(wèn)題2,我們期待循環(huán)永遠(yuǎn)不會(huì)停下來(lái),但實(shí)際上循環(huán)5次之后停了下來(lái)。我們有理由猜測(cè):循環(huán)體中的arr與arr = append(arr, v)中的并非同一個(gè)。
由于兩段代碼的匯編代碼差不多,這里仍以上面的匯編代碼來(lái)分析。00106行是初始AX寄存器,也是循環(huán)的開(kāi)始,所以我們關(guān)注00106行之前的代碼。
根據(jù)上面的分析,在00040行已經(jīng)將數(shù)組內(nèi)容放到了AX寄存器里,00081行到00101行,將數(shù)組拷貝到autotmp_2變量?jī)?nèi),由SP所指向的棧頂。
在讀這段代碼的匯編時(shí),發(fā)現(xiàn)編譯器針對(duì)數(shù)組內(nèi)容做了一個(gè)小優(yōu)化,當(dāng)數(shù)組長(zhǎng)度小于5時(shí)候,編譯器會(huì)認(rèn)為這個(gè)數(shù)組只是臨時(shí)變量,會(huì)直接做棧上賦值,直接將數(shù)組內(nèi)容放到autotmp_2變量中(棧上),省略了從數(shù)據(jù)只讀區(qū)到AX的過(guò)程(即00040行),數(shù)組長(zhǎng)度小于5時(shí),匯編代碼如下:
0x0024 00036 (main.go:5) MOVQ $1, ""..autotmp_2+24(SP) 0x002d 00045 (main.go:5) MOVQ $2, ""..autotmp_2+32(SP) 0x0036 00054 (main.go:5) MOVQ $3, ""..autotmp_2+40(SP) 0x003f 00063 (main.go:5) XORL AX, AX
分析到這里,我們可以得到一段表示for循環(huán)的偽代碼:
temp := {1, 2, 3, 4, 5} for i := 0; i < 5; i++ { v := temp[i] }
由此我們可以得到結(jié)論:for-range時(shí)拷貝了被訪問(wèn)的列表(array、slice、hashmap等)。問(wèn)題2所帶的思考:當(dāng)數(shù)組比較大時(shí),for-range拷貝數(shù)組的開(kāi)銷也會(huì)比較大,在實(shí)際應(yīng)用中應(yīng)當(dāng)避免這個(gè)開(kāi)銷。
總結(jié)
從上面的匯編代碼分析過(guò)來(lái)看,總結(jié)兩點(diǎn):
1. 循環(huán)過(guò)程中位置變量,只會(huì)聲明一次,也就是說(shuō)每次循環(huán)位置變量的地址都是相同的。 2. for-range時(shí)拷貝了被訪問(wèn)的列表(array、slice、hashmap等)。
延申閱讀
A Quick Guide to Go's Assembler
到此這篇關(guān)于從Go匯編角度解讀for循環(huán)的文章就介紹到這了,更多相關(guān)匯編for循環(huán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ARM匯編判斷之如何用匯編判斷數(shù)組中正負(fù)數(shù)個(gè)數(shù)
這篇文章主要介紹了ARM匯編判斷之如何用匯編判斷數(shù)組中正負(fù)數(shù)個(gè)數(shù),在匯編語(yǔ)言中程序的基本框架是不變的,這里我們可以直接把正向遍歷的程序給貼過(guò)來(lái),然后我們思考怎么運(yùn)用判斷后綴把數(shù)組中正數(shù)和負(fù)數(shù)分開(kāi),進(jìn)行判斷個(gè)數(shù)和分別求和,需要的朋友可以參考下2022-04-04在vs2017中編寫匯編的實(shí)現(xiàn)(圖文)
這篇文章主要介紹了在vs2017中編寫匯編的實(shí)現(xiàn)(圖文),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03UEFI開(kāi)發(fā)實(shí)戰(zhàn)用戶交互界面使用說(shuō)明VFR文件
這篇文章主要為大家介紹了UEFI開(kāi)發(fā)實(shí)戰(zhàn)用戶交互界面使用說(shuō)明VFR文件,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06匯編語(yǔ)言DOSBox及debug.exe在Windows64下環(huán)境搭建
這篇文章主要為大家介紹了匯編語(yǔ)言環(huán)境的搭建DOSBox及debug.exe在Windows64下安裝配置過(guò)程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11