背景
偶然发现项目代码中有如下的写法:
1 | type Student struct { |
我们且不去探讨“这样写法有什么作用”、“为什么要这样写”,我们先就代码本身讨论。
众所周知,从严格意义上来说,golang函数调用中只有值传递的方式传递参数,所以这个函数会返回一个新的指针,并指向一个新的对象。
然后我就在思考,这种函数会被编译器特殊优化么?
探索
写一个简单demo测试一下:
1 | package main |
运行结果很amazing啊
1 | $ go run main.go |
两个指针居然完全相等,这不合常理啊,讲道理,值传递的话,return返回出来的应该是一个新对象的指针啊
不过仔细观察一下,这个例子中,A是一个空struct,函数里也不可能修改传进来的A的实例a
,所以即使两个是相同的指针也没有任何影响。
那我们就去看看编译器是怎么做的吧。
-gcflags='-S'
列出汇编代码
FUNCDATA
和PCDATA
是编译器产生的,用于保存一些给垃圾收集的信息,在本例中可以过滤掉
1 | $ go build -gcflags='-S' main.go 2>&1 | grep -v "FUNCDATA\|PCDATA" | grep -A10 '"".NewStructA STEXT' |
$0-8
中0代表的是分配的栈帧大小为0,8代表的是各个实参和返回值的总大小
LEAQ runtime.zerobase(SB), AX
把runtime.zerobase(SB)
的地址传送到AX
寄存器
MOVQ AX, "".~r1+8(SP)
把AX
寄存器中的数据移动到~r1+8(SP)
即返回值的位置
runtime.zerobase
是个什么东西?
翻看源码找到runtime/malloc.go
826行
1 | // base address for all 0-byte allocations |
是一个用于所有对内存零占用的结构的地址
所以空struct的地址就是zerobase,所以函数NewStructA
才会返回相同的地址。
而且实际上前文所说的$0-8中的8代表的是各个实参和返回值的总大小
,这个8
也可以理解了,实参其实并没有占用栈空间,是返回值(空struct的指针)占用了栈空间的8个字节。
1 | package main |
空struct特性
以前一直不理解
chan struct{}
的意图,其实可以理解为struct{}
是golang中最小的数据结构1
2
3
4
5
6done := make(chan struct{})
go func() {
// ...
done <- struct{}{}
}()
<- donemap[string]struct{}
比map[string]bool
更节约空间var x [1000000000]struct{} fmt.Println(unsafe.Sizeof(x)) // print 0
参考资料
附录
助记符 | 指令种类 | 用途 | 示例 |
---|---|---|---|
MOVQ | 传送 | 数据传送 | MOVQ 48, AX // 把 48 传送到 AX |
LEAQ | 传送 | 地址传送 | LEAQ AX, BX // 把 AX 有效地址传送到 BX |
PUSHQ | 传送 | 栈压入 | PUSHQ AX // 将 AX 内容送入栈顶位置 |
POPQ | 传送 | 栈弹出 | POPQ AX // 弹出栈顶数据后修改栈顶指针 |
ADDQ | 运算 | 相加并赋值 | ADDQ BX, AX // 等价于 AX+=BX |
SUBQ | 运算 | 相减并赋值 | SUBQ BX, AX // 等价于 AX-=BX |
CMPQ | 运算 | 比较大小 | CMPQ SI CX // 比较 SI 和 CX 的大小 |
CALL | 转移 | 调用函数 | CALL runtime.printnl(SB) // 发起调用 |
JMP | 转移 | 无条件转移指令 | JMP 0x0185 //无条件转至 0x0185 地址处 |
JLS | 转移 | 条件转移指令 | JLS 0x0185 //左边小于右边,则跳到 0x0185 |