从Go汇编角度解读for循环的问题
Go常用的遍历方式有两种:for和for-range。实际上,for-range也只是for的语法糖,本文试图从汇编代码入手解释for循环是如何工作的。
问题
首先来看看几个令人迷惑的地方。
问题1:遍历过程中取值
funcmain(){ arr:=[5]int{1,2,3,4,5} for_,v:=rangearr{ println(&v) } }
上面这段代码里,会打印出什么?
问题2:遍历过程中修改
arr:=[]int{1,2,3,4,5} forv:=rangearr{ arr=append(arr,v) }
上面这段代码里,遍历前后arr有哪些变化?
窥探虚实
对于问题1,我们期待会打印出5个不同的地址,实际上最终打印出来的都是同一个地址,我们可以猜测v在循环过程中只声明了一次。看看问题1的汇编代码:
0x002800040(main.go:4)MOVQ""..stmp_0(SB),AX 0x002f00047(main.go:4)MOVQAX,"".arr+24(SP) 0x003400052(main.go:4)MOVUPS""..stmp_0+8(SB),X0 0x003b00059(main.go:4)MOVUPSX0,"".arr+32(SP) 0x004000064(main.go:4)MOVUPS""..stmp_0+24(SB),X0 0x004700071(main.go:4)MOVUPSX0,"".arr+48(SP) 0x004c00076(main.go:5)MOVQ"".arr+24(SP),AX 0x005100081(main.go:5)MOVQAX,""..autotmp_2+64(SP) 0x005600086(main.go:5)MOVUPS"".arr+32(SP),X0 0x005b00091(main.go:5)MOVUPSX0,""..autotmp_2+72(SP) 0x006000096(main.go:5)MOVUPS"".arr+48(SP),X0 0x006500101(main.go:5)MOVUPSX0,""..autotmp_2+88(SP) 0x006a00106(main.go:5)XORLAX,AX 0x006c00108(main.go:5)JMP162 0x006e00110(main.go:5)MOVQAX,""..autotmp_7+16(SP) 0x007300115(main.go:5)MOVQ""..autotmp_2+64(SP)(AX*8),CX 0x007800120(main.go:5)MOVQCX,"".v+8(SP) 0x007d00125(main.go:6)CALLruntime.printlock(SB) 0x008200130(main.go:6)LEAQ"".v+8(SP),AX 0x008700135(main.go:6)MOVQAX,(SP) 0x008b00139(main.go:6)CALLruntime.printpointer(SB) 0x009000144(main.go:6)CALLruntime.printnl(SB) 0x009500149(main.go:6)CALLruntime.printunlock(SB) 0x009a00154(main.go:5)MOVQ""..autotmp_7+16(SP),AX 0x009f00159(main.go:5)INCQAX 0x00a200162(main.go:5)CMPQAX,$5 0x00a600166(main.go:5)JLT110
00040行:MOVQ""..stmp_0(SB),AX将stmp_0变量里的内容放到AX寄存器里,stmp_0实际上就是arr数组,在生成的汇编代码里:
""..stmp_0SRODATAsize=40 0x000001000000000000000200000000000000 0x001003000000000000000400000000000000 0x00200500000000000000
由此可以看到stmp_0正是arr数组。
00106行:XORLAXAX是初始化AX寄存器,AX寄存器里包含当前循环位置。00108行:JMP162表示跳转到00162行。00162行:CMPQAX$5比较寄存器AX和5,伪代码:i<5,如果满足条件,则跳转到00110行。
00110行00159行为循环体代码,注意到00159行INCQAX,意即AX寄存器值自增,到这里我们可以大致分析出来for-range在汇编层面的伪代码:
fori:=0;i<5;i++{ }
这也就验证了上面说的for-range只是普通for的语法糖。
00110到00120行是循环体代码的前半部分。从Go汇编文档上看:SP寄存器指向当前栈帧的局部变量的开始位置,也就是说局部变量放在了SP寄存器的栈帧里。
00115行:MOVQ""..autotmp_2+64(SP)(AX*8),CX,autotmp_*是为临时变量自动生成的名字,这行汇编做的事情是将某个v值(注意,是值)放在CX寄存器里。
00120行:MOVQCX,"".v+8(SP)将CX寄存器里的内容放在SP寄存器指向的位置,00125行代码是一个隔断,00125之后的代码与println有关。重点在这行代码,每次循环都会将值放在"".v+8(SP)这个位置,在这个循环体代码里,我们并没有看到其他的临时变量声明,到这里,我们可以总结出:"".v+8(SP)这个位置就是变量v在栈帧中的位置,由于位置一直没有发生变化,在进行&v操作时取到的会是同一个地址。
对于问题1,根据汇编代码的分析,我们得出结论:v在循环过程中只会声明一次,每次循环只是将v值替换,并未重新声明临时变量,这样解释了问题1代码的输出结果。
再回到问题2,我们期待循环永远不会停下来,但实际上循环5次之后停了下来。我们有理由猜测:循环体中的arr与arr=append(arr,v)中的并非同一个。
由于两段代码的汇编代码差不多,这里仍以上面的汇编代码来分析。00106行是初始AX寄存器,也是循环的开始,所以我们关注00106行之前的代码。
根据上面的分析,在00040行已经将数组内容放到了AX寄存器里,00081行到00101行,将数组拷贝到autotmp_2变量内,由SP所指向的栈顶。
在读这段代码的汇编时,发现编译器针对数组内容做了一个小优化,当数组长度小于5时候,编译器会认为这个数组只是临时变量,会直接做栈上赋值,直接将数组内容放到autotmp_2变量中(栈上),省略了从数据只读区到AX的过程(即00040行),数组长度小于5时,汇编代码如下:
0x002400036(main.go:5)MOVQ$1,""..autotmp_2+24(SP) 0x002d00045(main.go:5)MOVQ$2,""..autotmp_2+32(SP) 0x003600054(main.go:5)MOVQ$3,""..autotmp_2+40(SP) 0x003f00063(main.go:5)XORLAX,AX
分析到这里,我们可以得到一段表示for循环的伪代码:
temp:={1,2,3,4,5} fori:=0;i<5;i++{ v:=temp[i] }
由此我们可以得到结论:for-range时拷贝了被访问的列表(array、slice、hashmap等)。问题2所带的思考:当数组比较大时,for-range拷贝数组的开销也会比较大,在实际应用中应当避免这个开销。
总结
从上面的汇编代码分析过来看,总结两点:
1.循环过程中位置变量,只会声明一次,也就是说每次循环位置变量的地址都是相同的。2.for-range时拷贝了被访问的列表(array、slice、hashmap等)。
延申阅读
AQuickGuidetoGo'sAssembler
到此这篇关于从Go汇编角度解读for循环的文章就介绍到这了,更多相关汇编for循环内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。