为什么写这篇文章?

昨天在技术群上,有人问了个问题:

如果一个结构体, 只是读里面的成员, 在 golang 里面传值的时候, 不传递指针, golang 编译器会帮你优化成 const & 么?

随便一猜:golang 肯定是直接 copy 整个结构体。

为了确认是否真的是这样,最直白的方式就是直接看 golang 生成的汇编代码。

从图中的汇编代码中,我们可以清楚的看到:golang 的确是执行了完整的结构体 copy 。


然后群友给了这样的反馈...

看着自己日益升高的发际线,我陷入了沉思...


好了,进入正题。

本文将以 1 + ... + 100 的代码为例,介绍以下几种语言查看“汇编代码”的方式。
(这里的“汇编代码”只是个统称,大家不用太计较)

  1. Golang
  2. Lua
  3. JavaScript(V8)
  4. Rust
  5. Python
  6. 等等 ...


1. Golang 生成汇编代码

源码

查看方式

分析
行 1 : 表示 将数值 1 放到 AX 中
行 2 : 表示 跳转到 行 4
行 3 : 表示 对 AX 中的数值执行 + 1 操作
行 4 : 比较 AX 是否在 100 以内。(0x64 是 数值 100 的 16 进制)
行 5 : 跳转到 行 3


嗯... 怎么感觉就是在空循环,我们的 sum 变量哪里去了?
事实上:golang 检测到 sum 没有被使用,直接就帮我们优化掉了,只留下一个空循环。
(但它没有彻底的帮我们把这个空循环也删掉...)

如果我们把代码改成这样子:


那么 sum 累加部分的汇编就是可以正常的显示出来,如图所示:


2. Lua 生成汇编代码

源码

查看方式

分析

Lua 虚拟机是基于寄存器来实现的。这段汇编代码读起来,就不像golang那么好理解了。
(关于寄存器虚拟机的相关内容,我在文章的评论中补充了一些。有兴趣的话,可以看看)

这里我就简单的分析下:

行 1-4:将常量表里的 0,1,100,1分别加载到寄存器中。
LOADK 指令后面的跟着 2 个参数,分别是:参数1 寄存器索引,参数2 常量表索引
分号后面的数值是: 具体的常量值

这四个数字中:
第一个数字 0 ,就是 sum 的初始值 。
后面三个数(1, 100, 1):表示了循环从 1 开始,到 100 结束,步长为 1
( 也就是说 i 从 1 开始,每次循环自动+1,直到达到 100)

行 5-7:执行循环
Lua 通过 FORPREP、FORLOOP 两条指令来实现循环。
它们的第一个参数:表示指向 循环所需的三个数字 的起始寄存器索引,也就是 寄存器 1。
(这样虚拟机就知道了,循环所需的三个数字:1,100,1,从而准确的控制循环的逻辑)
它们的第二个参数,表示PC指令跳转的距离。

注意:FORLOOP 会把 i+1 的结果 放在寄存器 4 中,对应 add 的第 3 个参数

行 6:执行 Add 操作
Add 后面跟着 3 个参数。含义如下:
参数1:存放结果的寄存器索引
参数2、参数3: 分别是两个加数的索引位置


既然已经分析了 golang 和 lua 两个语言生成的汇编代码,后面的语言就不再详细的分析了,基本大同小异。


3. JavaScript 生成汇编代码

查看方式


4. Rust 生成汇编代码

rust 就比较有意思了(和 c++ 差不多),值得稍微提一下。

查看方式

加入了 -O 优化之后,生成的汇编代码是这个样子,它直接用 5050 来赋值,省略了计算的过程。


5. Python 生成汇编代码

查看方式


其它语言:有时间再补充

...


最后

那么了解汇编层面的东西,有什么用呢?

嗯...对于绝大多数开发者来说,的确没什么用...

但是如果你有深入底层的追求,那么了解汇编的知识,会让你对程序理解的更加透彻。

而且,不管是 Rust,还是 C++ 都支持 内联汇编:可以在代码里嵌入汇编语言,直接控制 CPU,从而获取极致的底层操作、以及性能上的提升。

比如:腾讯开源的C++协程库:Tencent/libco 就用到了该技术方案。



补充说明:详见文章评论