注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
序 问题回顾本文章主要是为了回答上一篇文章的问题,并且介绍一下在这个过程中以及后续都会使用到的调试工具 gdb
请查看下面一段代码,会输出什么,为什么?A: 5 8 B: 8 8 C: 5 5 D: 5 6 1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" )
func main () { s := []int {1 , 2 } s = append (s, 3 , 4 , 5 ) fmt.Println(len (s), cap (s)) }
在讲解 slice 初始化的过程中,为什么 s2 , s3 打印的数组指针都是 0x587450 ? slice 扩容算法是否遗漏了什么?首先我们回顾一下上一篇文章当中讲到的 slice 扩容的 算法:
如果需要的最小容量比两倍原有容量大,那么就取需要的容量 如果原有 slice 长度小于 1024 那么每次就扩容为原来的两倍 如果原 slice 大于等于 1024 那么每次扩容就扩为原来的 1.25 倍 按照这个逻辑进行套用:
s 初始化时 len=2s = append(s, 3, 4, 5) 此时需要的最小容量为 5 > 2*2按照这个逻辑最后的答案应该是 C: 5 5 但是如果大家运行过这段代码应该会知道这个答案是 D: 5 6 为什么呢?这个逻辑和我们之前了解到的不太一样啊 接下来我们就使用 gdb 调试一下看看结果
调试 go 程序我们常用的调试工具其实是 dlv 这个工具非常好用,并且可以很好的和 VS Code Goland 等 IDE 进行结合,但是它无法调试 runtime 的代码,这个时候就要使用上 gdb 了,如果大家不需要调试 runtime 的代码的话还是建议使用 dlv
安装1 brew install gdb # for mac
如果是 linux 使用自带的包管理工具进行安装即可 注意安装完成之后需要在 home 目录上添加相关配置
1 2 3 4 vim ~/.gdbinit# 输入下面 add-auto-load-safe-path $GOROOT /src/runtime/runtime-gdb.py # 这里将 $GOROOT 替换为你的 GO 安装目录
如果你是 mac 首次安装 gdb 需要给 gdb 签名,可以参考: https://gist.github.com/hlissner/898b7dfc0a3b63824a70e15cd0180154
编译代码我们使用 -gcflags=all="-N -l" 禁用内联优化方便后面调试 在 mac 上如果不加 -ldflags='-compressdwarf=false' 在 gdb 调试的时候可能会提示 No symbol table is loaded. Use the "file" command.这是因为为了减少二进制大小会默认压缩 DWARF 调试信息,这个在 mac 和 windows 上部分工具不支持,linux 一般没有这个问题 加上 -ldflags='-compressdwarf=false' 这个标志可以禁用压缩方便调试 1 go build -o bin/03_q1_slice_cap -gcflags=all="-N -l" -ldflags='-compressdwarf=false' 02_array/03_q1_slice_cap/main.go
进入 GDB 调试窗口1 gdb -tui ./bin/03_q1_slice_cap
如下图所示,回车几次之后出现 Loading Go Runtime support. 就说明正常了
常用调试命令b 文件名:行数 打断点info b 当前的断点情况r 运行程序知道断点处c 继续执行到下一个断点s 单步执行,如果有调用函数则进入函数,注意和 n 的区别n 单步执行,如果有调用的函数不会进入函数内部until 退出循环until:行号 执行到指定行info locals 当前堆栈的所有变量info args 打印参数info goroutines 查看所有的 goroutine 及其 IDhelp 帮助q 退出 调试知道如何操作之后,我们开始干活
首先给 append 函数打一个断点 b main.go:11 然后执行到断点处 r 如下图所示,单步运行进入到 slice 的扩容函数中 s
接下来我们使用 n 一直单步执行到扩容算法结束,使用 info locals 打印变量。我们可以发现这个时候计算出的 newcap 和我们最初预计的一样是 5,那哪里出了问题呢?我们继续使用 n 接着看
如下图所示,执行到这里我们发现, newcap 被重新赋值了,并且这个时候 capmem=48 PtrSize=8 所以最后的出来了 newcap 等于 6 知道问题出现在哪里之后我们可以再来看一下源代码,有一个内存对齐的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Returns size of the memory block that mallocgc will allocate if you ask for the size. func roundupsize (size uintptr ) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr (class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) } else { return uintptr (class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) } } if size+_PageSize < size { return size } return alignUp(size, _PageSize) }const ( _MaxSmallSize = 32768 smallSizeDiv = 8 smallSizeMax = 1024 largeSizeDiv = 128 _NumSizeClasses = 67 _PageShift = 13 )
var class_to_size = [_NumSizeClasses]uint16 {0 , 8 , 16 , 32 , 48 , 64 , 80 , 96 , 112 , ...}var class_to_allocnpages = [_NumSizeClasses]uint8 {0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , ...}
可以发现我们之前在计算 capmem 的时候传入的 capmem = roundupsize(uintptr(newcap) * sys.PtrSize) 值是 5*8=40 最后对齐出的结果就是 48 了
总结 slice 扩容算法如果需要的最小容量比两倍原有容量大,那么就取需要的容量 如果原有 slice 长度小于 1024 那么每次就扩容为原来的两倍 如果原 slice 大于等于 1024 那么每次扩容就扩为原来的 1.25 倍 除此之外扩容容量计算完成之后,还会进行一次内存对齐操作 搜索 slice 扩容策略很多都会说第 2、3 点但是没有说 1, 4 点就会造成一些困惑
GDB 调试mac 下安装使用 gdb 比较麻烦,建议使用 linux 一般情况下我们还是建议使用 dlv 进行调试,如果有 runtime 库的调试需求可以使用 gdb 学习了 gdb 的安装以及基本使用方式,你之前有类似的经历么?一般会在什么情况下使用 gdb 进行调试 问题 2 解答: 在讲解 slice 初始化的过程中,为什么 s2 , s3 打印的数组指针都是 0x587450 ?在讲解 makeslice 的时候我们有说到最后一步会调用 mallocgc 分配内存,看这个函数的源码我们就能发现,有一步判断,如果容量为 0 会返回一个固定的地址
1 2 3 if size == 0 { return unsafe.Pointer(&zerobase) }
关注我获取更新猜你喜欢