做塑胶材料的网站,成都网站制作怎么收费,注册销售公司流程和费用,wordpress 概要文章目录 背景分析代码疑问 直接上汇编gdb调试优化后的汇编staticunit64s查看禁止优化后的汇编 查看编译过程的SSA生成SSAb对应的SSAc对应的SSAgo官方文档的解释 对比C语言的表现总结 背景
网上看到一段代码#xff0c;来源是Golang 编译器优化那些事#xff0c;百思不得其解… 文章目录 背景分析代码疑问 直接上汇编gdb调试优化后的汇编staticunit64s查看禁止优化后的汇编 查看编译过程的SSA生成SSAb对应的SSAc对应的SSAgo官方文档的解释 对比C语言的表现总结 背景
网上看到一段代码来源是Golang 编译器优化那些事百思不得其解。本篇文章主要是分析以下代码一方面是知其然知其所以然另一方面也是避免以后掉进类似的深坑。
分析代码
func main() {var a byte (1 uint(len(s))) / 128 // 编译器计算var b byte (1 uint(len(s[:]))) / 128 // 运行时计算, 溢出了var c byte byte(int(1uint(len(s[:]))) / 128) // 运行时计算用int 所以没有溢出fmt.Println(a, b, c)
}# 输出结果
a:4
b:0:
c:4从直觉上来看a,b,c的行为应该是类似的区别是a的len(s)可以直接获取到长度b和c因为涉及到切片操作需要在runtime层计算。但这个结果还是很费解。 疑问
a在汇编的时候表现是什么样子的为什么说是编译器计算b为什么是0512/1284才对? 为什么是0c和b的行为也不一样只是多了个int类型转换以上行为出现的理论支撑是什么 直接上汇编
# 带优化的编译
go build -o demo ./demo.go
# 输出汇编查看demo的main函数
go tool objdump -S -s main.main demo# 结果
func main() {0x4808e0 493b6610 CMPQ SP, 0x10(R14)0x4808e4 7673 JBE 0x4809590x4808e6 55 PUSHQ BP0x4808e7 4889e5 MOVQ SP, BP0x4808ea 4883ec58 SUBQ $0x58, SPfmt.Println(a, b, c)从最终汇编上看不出来什么东西因为编译器做了太多优化。我们可以用gdb调试去查看生成的汇编以及对应的代码行会更加清晰一些。关于gdb调试go程序可以参考我之前的文章Golang汇编之通过map地址找到value的值_golang如何获取map中的每个value-CSDN博客 gdb调试优化后的汇编
gdb demo // 开始调试
b main.main // 打断点
run // 运行程序
layout split // 查看源码和汇编
si // 根据汇编指令一次一行的查看结果如下
abc在最终汇编中都是常量编译器做过优化了以上汇编看不到内部都经过哪些转换以及runtime层都做了什么出现了runtime.staticunit64s? 这个是什么意思 staticunit64s
在go源码中通过rg搜索 staticunit64s发现在runtime/iface.go中有如下代码。看起来是为了避免分配小整数值。这里一共有256个通过地址偏移量来找到0-255的值。例如上面汇编中的staticuint64s32意思是偏移32为打印地址结果确实是4.
可以参考 https://g4s8.wtf/posts/go-low-latency-one/ 查看禁止优化后的汇编
# 编译命令: go build -gcflags-S -N -l demo.goFUNCDATA $2, main.main.stkobj(SB)MOVB $4, main.a33(SP)LEAQ go:string.123456789(SB), DXMOVQ DX, main..autotmp_3120(SP)MOVQ $9, main..autotmp_3128(SP)MOVQ $9, main..autotmp_440(SP)MOVB $0, main.b32(SP)LEAQ go:string.123456789(SB), DXMOVQ DX, main..autotmp_3120(SP)MOVQ $9, main..autotmp_3128(SP)MOVQ $9, main..autotmp_440(SP)MOVB $4, main.c31(SP)这里可以看到变量a直接作为常量赋值了。变量b和变量c是有计算过程的但是有一些注意的点
MOVQ: 用于将64位的数据从源操作数移动到目标操作数。
MOVB: 用于将8位的数据从源操作数移动到目标操作数。从代码上也能理解毕竟我们的a,b,c类型都是byte类型使用MOVB是合适的。
main…autotmp_3-16(SP) 和 main.b-112(SP) 实际上指向堆栈指针 SP 的偏移位置。autotmp_xx是 runtime 临时变量反映当前栈帧指针位置。
所以现在的疑问变成了中间计算过程的临时变量类型是什么是int还是byte? 查看编译过程的SSA 生成SSA
GOSSAFUNCmain go build -gcflags-N -l ./demo.go默认会在当前目录生成个ssa.html我们可以浏览器打开查看。 b对应的SSA 可以看到中间计算过程的临时变量类型都是byte。那么基本上破案了byte的类型范围是0-255我们中间计算的1 9的结果是512从这一步开始就溢出了所以结果为0. 0/128的结果还是0因此b0! c对应的SSA 可以看到在计算c的时候因为有int转换类型中间变量类型都是int类型因此没有溢出。 go官方文档的解释
以上go编译器的行为理论上来说都能从go的文档中找到蛛丝马迹。我们可以搜索go spec来查看文档。说实话没有找到直接关于这部分的描述只有以下一句比较符合链接 https://go.dev/ref/spec#Constants
An untyped constant has a default type which is the type to which the
constant is implicitly converted in contexts where a typed value is
required, for instance, in a short variable declaration such as
i : 0 where there is no explicit type. The default type of an
untyped constant is bool, rune, int, float64, complex128,
or string respectively, depending on whether it is a boolean, rune,
integer, floating-point, complex, or string constant.对于没有明确类型指定的常量来说会默认转换成目标类型也就是byte. 虽然可以这么理解但仍然是比较反直觉的我们可以看看c语言的表现如何。 对比C语言的表现
#include stdio.h
#include string.h
#include stdint.h // For uint8_t// b4
void calculate_and_print_b_4(char* s) {unsigned char b (1 strlen(s)) / 128;printf(b: %u\n, b);
}// b0
void calculate_and_print_b_0(char* s) {unsigned char b (uint8_t)(1 strlen(s)) / 128;printf(b: %u\n, b);
}int main() {char s[] 123456789;calculate_and_print_b_0(s);return 0;
}通过gcc编译然后运行查看
gcc test.c
./a.out我们可以看到如果仿照go的写法默认输出是4而不是0. C语言是计算结束然后转换成目标类型和go的表现不一样更符合直觉。
如果想重现b0也简单直接把中间的类型强制转换成uint8即可。 总结
为什么要分析这段程序主要是觉得坑挺多的一不小心代码就会出bug而且还是难以排查的bug。想要避免类似的问题要么是熟悉go的规范要么就是尽量写一些“朴实无华”的代码。
越发明白大佬说的那句话编程语言玩到最后玩的就是规范。虽然go的规范有点晦涩难懂但我们可以通过遇到的问题去跟规范对照起来理解也算是一种进步的方式吧。尽量不要错过深究的机会也许深究下去会得到更多呢end