我擦~字符串转字节切片后,切片的容量竟然千奇百怪
来自公众号:新世界杂货铺
神奇的现象
切片, 切片, 又是切片!
前一篇文章讲的是切片, 今天遇到的神奇问题还是和切片有关, 具体怎么个神奇法, 我们来看看下面几个现象
现象一
现象二
现象三
现象四
现象五
分析
到这儿我已经满脑子问号了
字符串变量转切片
一个小小的字符串转切片, 内部究竟发生了什么, 竟然如此的神奇。 这种时候只好祭出前一篇文章的套路了, 看看汇编代码(希望之后有机会能够对go的汇编语法进行简单的介绍
)有没有什么关键词能够帮助我们
以下为现象一转换的汇编代码关键部分
以下为现象二转换的汇编代码关键部分
在看汇编代码之前, 我们首先来看一看runtime.stringtoslicebyte
的函数签名
到这里只靠关键词已经无法看出更多的信息了,还是需要稍微了解一下汇编的语法,笔者在这里列出一点简单的分析, 之后我们还是可以通过取巧的方法发现更多的东西
通过上面汇编代码的分析可以知道,现象一和现象二的区别就是传递给runtime.stringtoslicebyte
的第一个参数不同。 通过对runtime包中stringtoslicebyte
函数分析,第一个参数是否有值和字符串长度会影响代码执行的分支,从而生成不同的切片, 因此容量不一样也是常理之中, 下面我们看源码
然而, stringtoslicebyte的第一个参数什么情况下才会有值,什么情况下为nil, 我们仍然不清楚。那怎么办呢, 只好祭出全局搜索大法:
最终在go的编译器源码cmd/compile/internal/gc/walk.go发现了如下代码块
我们查看mkcall
函数签名可以知道, 从第四个参数开始的所有变量都会作为参数传递给第一个参数对应的函数, 最后生成一个*Node
的变量。其中Node结构体解释如下:
综合上述信息我们得出的结论是,编译器会对stringtoslicebyte的函数调用生成一个AST(抽象语法树)对应的节点。因此我们也知道传递给stringtoslicebyte函数的第一个变量也就对应于上图中的变量a.
其中a的初始值为nodnil()
的返回值,即默认为nil
. 但是n.Esc == EscNone
时,a会变成一个数组。我们看一下EscNone的解释.
由上可知, EscNone
用来判断变量是否逃逸,到这儿了我们就很好办了,接下来我们对现象一和现象二的代码进行逃逸分析.
根据上面的信息我们知道在现象一中,bs变量发生了逃逸,现象二中变量未发生逃逸,也就是说stringtoslicebyte函数的第一个参数在变量未发生逃逸时其值不为nil,变量发生逃逸时其值为nil。到这里我们已经搞明白stringtoslicebyte的第一个参数了, 那我们继续分析stringtoslicebyte的内部逻辑
我们在runtime/string.go中看到stringtoslicebyte第一个参数的类型定义如下:
综上: 现象二中bs变量未发生变量逃逸, stringtoslicebyte第一个参数不为空且是一个长度为32的byte数组, 因此在现象二中生成了一个容量为32的切片
根据对stringtoslicebyte的源码分析, 我们知道现象一调用了rawbyteslice
函数
由上面的代码知道, 切片的容量通过runtime/msize.go中的roundupsize
函数计算得出, 其中MaxSmallSize和classto_size均定义在[runtime/sizeclasses.go](https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go#L75)
由于字符串abc的长度小于MaxSmallSize(32768),故切片的长度只能取数组classto_size中的值, 即0, 8, 16, 32, 48, 64, 80, 96, 112, 128....
s
至此, 现象一中切片容量为什么为8也真相大白了。相信到这里很多人已经明白现象四和现象五是怎么回事儿了, 其逻辑分别与现象一和现象二是一致的, 有兴趣的, 可以在自己的电脑上面试一试。
字符串直接转切片
那你说了这么多, 现象三还是不能解释啊。请各位看官莫急, 接下来我们继续分析。
相信各位细心的小伙伴应该早就发现了我们在上面的cmd/compile/internal/gc/walk.go
源码图中折叠了部分代码, 现在我们就将这块神秘的代码赤裸裸的展示出来
我们分析这块代码发现,go编译器在将字符串转字节切片
生成AST时,总共分为三步。
先判断该变量是否是常量字符串,如果是常量字符串,则直接通过
types.NewArray
创建一个和字符串等长的数组
常量字符串生成的切片变量也要进行逃逸分析,并判断其大小是否大于函数栈允许分配给变量的最大长度, 从而判断节点是分配在栈上还是在堆上
最后,如果字符串长度是大于0, 将字符串内容复制到字节切片中, 然后返回。因此现象三中的切片容量是3也就完全清楚了
结论
字符串转字节切片步骤如下
判断是否是常量, 如果是常量则转换为等容量等长的字节切片
如果是变量, 先判断生成的切片是否发生变量逃逸
* 如果逃逸或者字符串长度>32, 则根据字符串长度可以计算出不同的容量
* 如果未逃逸且字符串长度<=32, 则字符切片容量为32
扩展
常见逃逸情况
函数返回局部指针
栈空间不足逃逸
动态类型逃逸, 很多函数参数为interface类型,比如fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型, 也会发生逃逸
闭包引用对象逃逸
注: 写本文时, 笔者所用go版本为: go1.13.4
参考
https://golang.org/src/cmd/compile/README.md
https://my.oschina.net/renhc/blog/2222104
生命不息, 探索不止, 后续将持续更新有关于go的技术探索
原创不易, 卑微求关注收藏二连.
版权声明: 本文为 InfoQ 作者【新世界杂货铺】的原创文章。
原文链接:【http://xie.infoq.cn/article/2b6cf879be34fa86378e06f7c】。未经作者许可,禁止转载。
评论