从Go汇编角度解读for循环的问题

Go常用的遍历方式有两种:for和for-range。实际上,for-range也只是for的语法糖,本文试图从汇编代码入手解释for循环是如何工作的。

问题

首先来看看几个令人迷惑的地方。

问题1:遍历过程中取值

func main() {
 arr := [5]int{1, 2, 3, 4, 5}
 for _, v := range arr {
  println(&v)
 }
}

上面这段代码里,会打印出什么?

问题2:遍历过程中修改

arr := []int{1, 2, 3, 4, 5}
for v := range arr {
 arr = append(arr, v)
}

上面这段代码里,遍历前后arr有哪些变化?

窥探虚实

对于问题1,我们期待会打印出5个不同的地址,实际上最终打印出来的都是同一个地址,我们可以猜测v在循环过程中只声明了一次。看看问题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变量里的内容放到AX寄存器里,stmp_0实际上就是arr数组,在生成的汇编代码里:

""..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数组。

00106行:XORL AX AX是初始化AX寄存器,AX寄存器里包含当前循环位置。 00108行:JMP 162表示跳转到00162行。 00162行:CMPQ AX $5比较寄存器AX和5,伪代码:i < 5,如果满足条件,则跳转到00110行。

00110行00159行为循环体代码,注意到00159行INCQ AX, 意即AX寄存器值自增,到这里我们可以大致分析出来for-range在汇编层面的伪代码:

for i := 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行:MOVQ CX, "".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时,汇编代码如下:

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循环的伪代码:

temp := {1, 2, 3, 4, 5}
for i := 0; i < 5; i++ {
 v := temp[i]
}

由此我们可以得到结论:for-range时拷贝了被访问的列表(array、slice、hashmap等)。问题2所带的思考:当数组比较大时,for-range拷贝数组的开销也会比较大,在实际应用中应当避免这个开销。

总结

从上面的汇编代码分析过来看,总结两点:

1. 循环过程中位置变量,只会声明一次,也就是说每次循环位置变量的地址都是相同的。 2. for-range时拷贝了被访问的列表(array、slice、hashmap等)。

延申阅读

A Quick Guide to Go's Assembler

到此这篇关于从Go汇编角度解读for循环的文章就介绍到这了,更多相关汇编for循环内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 汇编分析 Golang 循环(推荐)

    女主宣言 今天小编为大家分享一篇关于Golang循环汇编分析的文章,文章中介绍了golang循环的汇编层面的处理,通过分析,我们可以更了解循环的实现.希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在" 3 60云计算 ",点关注哦! 循环是编程中很强大的一个概念,而且非常容易处理. 但是,必须将其翻译成机器可理解的基本指令. 它的编译方式也可能影响标准库中的其他组件. 让我们开始分析一下范围循环 . 1循环汇编 范围循环可以迭代数组,切片或通道.下面函数展示了,对分

  • 使用汇编语言实现if else 循环函数调用的具体方法

    需要使用汇编来演示如下代码 需要下载ollydbg汇编调试器 点击File-Open随意打开一个exe文件 我这里随便找到c:/windows/explorer.exe文件 这里EIP的值表示下一次运行需要执行的代码位置 双击 EIP红色地址 左边代码会自动跳转到对应的代码行 有了以下环节 接下来添加代码 如果替换的代码 占用的字节数 小于原始的代码数 会自动补充 nop空指令 一.实现 if else MOV EAX,1 表示将1立即数 设置给EAX寄存器 CMP EAX,1 比较EAX的值和

  • 详解汇编语言RCL(带进位循环左移)和RCR(带进位循环右移)指令

    汇编语言是依赖于计算机的低级的程序设计语言. RCL(带进位循环左移)指令把每一位都向左移,进位标志位复制到 LSB,而 MSB 复制到进位标志位: 如果把进位标志位当作操作数最高位的附加位,那么 RCL 就成了循环左移操作.下面的例子中,CLC 指令清除进位标志位.第一条 RCL 指令将 BL 最高位移入进位标志位,其他位都向左移一位.第二条 RCL 指令将进位标志位移入最低位,其他位都向左移一位: clc                             ; CF = 0 mov bl

  • 从Go汇编角度解读for循环的问题

    Go常用的遍历方式有两种:for和for-range.实际上,for-range也只是for的语法糖,本文试图从汇编代码入手解释for循环是如何工作的. 问题 首先来看看几个令人迷惑的地方. 问题1:遍历过程中取值 func main() { arr := [5]int{1, 2, 3, 4, 5} for _, v := range arr { println(&v) } } 上面这段代码里,会打印出什么? 问题2:遍历过程中修改 arr := []int{1, 2, 3, 4, 5} for

  • JVM详解之汇编角度理解本地变量的生命周期

    简介 java方法中定义的变量,它的生命周期是什么样的呢?是不是一定要等到方法结束,这个创建的对象才会被回收呢? 带着这个问题我们来看一下今天的这篇文章. 本地变量的生命周期 在类中,变量类型有类变量,成员变量和本地变量. 本地变量指的是定义在方法中的变量,如果我们在方法中定义了一个变量,那么这个变量的生命周期是怎么样的呢? 举个例子: public void test(){ Object object = new Object(); doSomeThingElse(){ ... } } 在上面

  • 浅谈JVM系列之从汇编角度分析NullCheck

    一个普通的virtual call 我们来分析一下在方法中调用list.add方法的例子: public class TestNull { public static void main(String[] args) throws InterruptedException { List<String> list= new ArrayList(); list.add("www.flydean.com"); for (int i = 0; i < 10000; i++)

  • 浅析Swift中struct与class的区别(汇编角度底层分析)

    概述 相对Objective-C, Swift使用结构体Struct的比例大大增加了,其中Int, Bool,以及String,Array等底层全部使用Struct来定义!在Swift中结构体不仅可以定义成员变量(属性),还可以定义成员方法,和类比较相似,都是具有定义和使用属性,方法以及初始化器等面向对象特性,但是结构体是不具有继承性,不具备运行时强制类型转换的以及引用计数等能力的! 下面来从汇编角度分析struct与class的区别! 基本知识 1.结构体 自动初始化器 在63行的调用中可以传

  • 从使用角度解读c++20 协程示例

    目录 协程长什么样子 c++20的协程三板斧 co_return co_yield co_await 理解协程 协程长什么样子 网上一堆乱七八糟的定义,看的人云里雾里,毫无意义.下面从实战角度看看协程到底长什么样子. 首先,类比线程,线程是个函数.把这个函数交给 创建线程的api,然后这个函数就变成线程了.这个函数本身没有任何特殊的地方,就是普通函数. 相比于线程,协程也是个函数,不过协程函数比线程函数讲究多了. 它必须要有返回值,返回值的类型 还必须’内嵌’一个promise_type类型pr

  • 解读CocosCreator源码之引擎启动与主循环

    前言 预备 不知道你有没有想过,假如把游戏世界比作一辆汽车,那么这辆"汽车"是如何启动,又是如何持续运转的呢? 如题,本文的内容主要为 Cocos Creator 引擎的启动流程和主循环. 而在主循环的内容中还会涉及到:组件的生命周期和计时器.缓动系统.动画系统和物理系统等... 本文会在宏观上为大家解读主循环与各个模块之间的关系,对于各个模块也会简单介绍,但不会深入到模块的具体实现. 因为如果把每个模块都"摸"一遍,那这篇文章怕是写不完了. Go! 希望大家看完这

  • 浅析Go汇编语法和MatrixOne使用介绍

    目录 MatrixOne数据库是什么? Go汇编介绍 为什么使用Go汇编? 为什么不用CGO? Go汇编语法特点 操作数顺序 寄存器宽度标识 函数调用约定 对写Go汇编代码有帮助的工具 avo text/template 在Go汇编代码中使用宏 在MatrixOne数据库中的Go语言汇编应用 基本向量运算加速 Go语言无法直接调用的指令 编译器无法达到的特殊优化效果 MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP.AP.流计算等多种负载的极简大数据引擎.MatrixO

  • 深入分析golang多值返回以及闭包的实现

    一.前言 golang有很多新颖的特性,不知道大家的使用的时候,有没想过,这些特性是如何实现的?当然你可能会说,不了解这些特性好像也不影响自己使用golang,你说的也有道理,但是,多了解底层的实现原理,对于在使用golang时的眼界是完全不一样的,就类似于看过http的实现之后,再来使用http框架,和未看过http框架时的眼界是不一样的,当然,你如果是一名it爱好者,求知欲自然会引导你去学习. 二.这篇文章主要就分析两点:      1.golang多值返回的实现;      2.golan

  • C/C++ 数组和指针及引用的区别

    C/C++ 数组和指针及引用的区别 1.数组和指针的区别 (1)定义 数组是一个符号,不是变量,因而没有自己对应的存储空间.但是,指针是一个变量,里面存储的内容是另外一个变量的地址,因为是变量所以指针有自己的内存空间,只不过里面存储的内容比较特殊. (2)区别 a.对于声明和定义,指针和数组是不相同的,定义为数组,则声明也应该是数组,不可混淆 b.当作下标操作符时,指针和数组是等价的.a[i]会被编译器翻译成*(a+i). c.当数组声明被用作函数形参的时候,数组实际会被当作指针来使用. (3)

  • Java Iterator迭代器_动力节点Java学院整理

    迭代器是一种模式,它可以使得对于序列类型的数据结构的遍历行为与被遍历的对象分离,即我们无需关心该序列的底层结构是什么样子的.只要拿到这个对象,使用迭代器就可以遍历这个对象的内部. 1.Iterator Java提供一个专门的迭代器<<interface>>Iterator,我们可以对某个序列实现该interface,来提供标准的Java迭代器.Iterator接口实现后的功能是"使用"一个迭代器. 文档定义: Package java.util; publici

随机推荐