你能一口说出go中字符串转字节切片的容量嘛?
神奇的现象
切片, 切片, 又是切片!
前一篇文章讲的是切片, 今天遇到的神奇问题还是和切片有关, 具体怎样个神奇法, 我们来看看下面几个现象
现象一
a := "abc"bs := []byte(a)fmt.Println(bs, len(bs), cap(bs))// 输出: [97 98 99] 3 8现象二
a := "abc"bs := []byte(a)fmt.Println(len(bs), cap(bs))// 输出: 3 32现象三
bs := []byte("abc")fmt.Println(len(bs), cap(bs))// 输出: 3 3现象四
a := ""bs := []byte(a)fmt.Println(bs, len(bs), cap(bs))// 输出: [] 0 0现象五
a := ""bs := []byte(a)fmt.Println(len(bs), cap(bs))// 输出: 0 32分析
到这儿我已经满脑子问号了
image
字符串变量转切片
一个小小的字符串转切片, 内部到底发生了什么, 竟然如此的神奇。 这种时候只好祭出前一篇文章的套路了, 看看汇编代码(希望之后有机会能够对go的汇编语法进行简单的详情)有没有什么关键词能够帮助我们
以下为现象一转换的汇编代码关键部分
"".main STEXT size=495 args=0x0 locals=0xd8 0x0000 00000 (test.go:5) TEXT "".main(SB), ABIInternal, $216-0 0x0000 00000 (test.go:5) MOVQ (TLS), CX 0x0009 00009 (test.go:5) LEAQ -88(SP), AX 0x000e 00014 (test.go:5) CMPQ AX, 16(CX) 0x0012 00018 (test.go:5) JLS 485 0x0018 00024 (test.go:5) SUBQ $216, SP 0x001f 00031 (test.go:5) MOVQ BP, 208(SP) 0x0027 00039 (test.go:5) LEAQ 208(SP), BP 0x002f 00047 (test.go:5) FUNCDATA $0, gclocals·7be4bbacbfdb05fb3044e36c22b41e8b(SB) 0x002f 00047 (test.go:5) FUNCDATA $1, gclocals·648d0b72bb9d7f59fbfdbee57a078eee(SB) 0x002f 00047 (test.go:5) FUNCDATA $2, gclocals·2dfddcc7190380b1ae77e69d81f0a101(SB) 0x002f 00047 (test.go:5) FUNCDATA $3, "".main.stkobj(SB) 0x002f 00047 (test.go:6) PCDATA $0, $1 0x002f 00047 (test.go:6) PCDATA $1, $0 0x002f 00047 (test.go:6) LEAQ go.string."abc"(SB), AX 0x0036 00054 (test.go:6) MOVQ AX, "".a+96(SP) 0x003b 00059 (test.go:6) MOVQ $3, "".a+104(SP) 0x0044 00068 (test.go:7) MOVQ $0, (SP) 0x004c 00076 (test.go:7) PCDATA $0, $0 0x004c 00076 (test.go:7) MOVQ AX, 8(SP) 0x0051 00081 (test.go:7) MOVQ $3, 16(SP) 0x005a 00090 (test.go:7) CALL runtime.stringtoslicebyte(SB) 0x005f 00095 (test.go:7) MOVQ 40(SP), AX 0x0064 00100 (test.go:7) MOVQ 32(SP), CX 0x0069 00105 (test.go:7) PCDATA $0, $2 0x0069 00105 (test.go:7) MOVQ 24(SP), DX 0x006e 00110 (test.go:7) PCDATA $0, $0 0x006e 00110 (test.go:7) PCDATA $1, $1 0x006e 00110 (test.go:7) MOVQ DX, "".bs+112(SP) 0x0073 00115 (test.go:7) MOVQ CX, "".bs+120(SP) 0x0078 00120 (test.go:7) MOVQ AX, "".bs+128(SP)以下为现象二转换的汇编代码关键部分
"".main STEXT size=393 args=0x0 locals=0xe0 0x0000 00000 (test.go:5) TEXT "".main(SB), ABIInternal, $224-0 0x0000 00000 (test.go:5) MOVQ (TLS), CX 0x0009 00009 (test.go:5) LEAQ -96(SP), AX 0x000e 00014 (test.go:5) CMPQ AX, 16(CX) 0x0012 00018 (test.go:5) JLS 383 0x0018 00024 (test.go:5) SUBQ $224, SP 0x001f 00031 (test.go:5) MOVQ BP, 216(SP) 0x0027 00039 (test.go:5) LEAQ 216(SP), BP 0x002f 00047 (test.go:5) FUNCDATA $0, gclocals·0ce64bbc7cfa5ef04d41c861de81a3d7(SB) 0x002f 00047 (test.go:5) FUNCDATA $1, gclocals·00590b99cfcd6d71bbbc6e05cb4f8bf8(SB) 0x002f 00047 (test.go:5) FUNCDATA $2, gclocals·8dcadbff7c52509cfe2d26e4d7d24689(SB) 0x002f 00047 (test.go:5) FUNCDATA $3, "".main.stkobj(SB) 0x002f 00047 (test.go:6) PCDATA $0, $1 0x002f 00047 (test.go:6) PCDATA $1, $0 0x002f 00047 (test.go:6) LEAQ go.string."abc"(SB), AX 0x0036 00054 (test.go:6) MOVQ AX, "".a+120(SP) 0x003b 00059 (test.go:6) MOVQ $3, "".a+128(SP) 0x0047 00071 (test.go:7) PCDATA $0, $2 0x0047 00071 (test.go:7) LEAQ ""..autotmp_5+64(SP), CX 0x004c 00076 (test.go:7) PCDATA $0, $1 0x004c 00076 (test.go:7) MOVQ CX, (SP) 0x0050 00080 (test.go:7) PCDATA $0, $0 0x0050 00080 (test.go:7) MOVQ AX, 8(SP) 0x0055 00085 (test.go:7) MOVQ $3, 16(SP) 0x005e 00094 (test.go:7) CALL runtime.stringtoslicebyte(SB) 0x0063 00099 (test.go:7) MOVQ 40(SP), AX 0x0068 00104 (test.go:7) MOVQ 32(SP), CX 0x006d 00109 (test.go:7) PCDATA $0, $3 0x006d 00109 (test.go:7) MOVQ 24(SP), DX 0x0072 00114 (test.go:7) PCDATA $0, $0 0x0072 00114 (test.go:7) PCDATA $1, $1 0x0072 00114 (test.go:7) MOVQ DX, "".bs+136(SP) 0x007a 00122 (test.go:7) MOVQ CX, "".bs+144(SP) 0x0082 00130 (test.go:7) MOVQ AX, "".bs+152(SP)在看汇编代码之前, 我们首先来看一看runtime.stringtoslicebyte的函数签名
func stringtoslicebyte(buf *tmpBuf, s string) []byte到这里只靠关键词已经无法看出更多的信息了,还是需要略微理解一下汇编的语法,笔者在这里列出一点简单的分析, 之后我们还是可以通过取巧的方法发现更多的东西
// 现象一给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6) LEAQ go.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6) MOVQ AX, "".a+96(SP) // 将AX中的内容存入变量a中0x003b 00059 (test.go:6) MOVQ $3, "".a+104(SP) // 将字符串长度3存入变量a中0x0044 00068 (test.go:7) MOVQ $0, (SP) // 将0 传递个runtime.stringtoslicebyte(SB)的第一个参数(笔者猜测对应go中的nil)0x004c 00076 (test.go:7) PCDATA $0, $0 // 据说和gc有关, 具体还不清楚, 一般情况可以忽略0x004c 00076 (test.go:7) MOVQ AX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0051 00081 (test.go:7) MOVQ $3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005a 00090 (test.go:7) CALL runtime.stringtoslicebyte(SB) // 调用函数, 此行后面的几行代码是将返回值赋值给变量bs// 现象二给runtime.stringtoslicebyte的传参0x002f 00047 (test.go:6) LEAQ go.string."abc"(SB), AX // 将字符串"abc"放入寄存器AX0x0036 00054 (test.go:6) MOVQ AX, "".a+120(SP) // 将AX中的内容存入变量a中0x003b 00059 (test.go:6) MOVQ $3, "".a+128(SP) // 将字符串长度3存入变量a中0x0047 00071 (test.go:7) PCDATA $0, $20x0047 00071 (test.go:7) LEAQ ""..autotmp_5+64(SP), CX // 将内部变量autotmp_5放入寄存器CX0x004c 00076 (test.go:7) PCDATA $0, $10x004c 00076 (test.go:7) MOVQ CX, (SP) // 将CX中的内容传递给runtime.stringtoslicebyte(SB)的第一个参数0x0050 00080 (test.go:7) PCDATA $0, $00x0050 00080 (test.go:7) MOVQ AX, 8(SP) // 将AX中的内容传递给runtime.stringtoslicebyte(SB)的第二个参数0x0055 00085 (test.go:7) MOVQ $3, 16(SP) // 将字符串长度传递给runtime.stringtoslicebyte(SB)的第二个参数0x005e 00094 (test.go:7) CALL runtime.stringtoslicebyte(SB)通过上面汇编代码的分析可以知道,现象一和现象二的区别就是传递给runtime.stringtoslicebyte的第一个参数不同。 通过对runtime包中stringtoslicebyte函数分析,第一个参数能否有值和字符串长度会影响代码执行的分支,从而生成不同的切片, 因而容量不一样也是常理之中, 下面我们看源码
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { b = rawbyteslice(len(s)) } copy(b, s) return b}然而, stringtoslicebyte的第一个参数什么情况下才会有值,什么情况下为nil, 我们依然不清楚。那怎样办呢, 只好祭出全局搜索大法:
# 在go源码根目录执行下面的命令grep stringtoslicebyte -r . | grep -v "//"最终在go的编译器源码cmd/compile/internal/gc/walk.go发现了如下代码块
image
我们查看mkcall 函数签名可以知道, 从第四个参数开始的所有变量都会作为参数传递给第一个参数对应的函数, 最后生成一个*Node的变量。其中Node结构体解释如下:
// A Node is a single node in the syntax tree.// Actually the syntax tree is a syntax DAG, because there is only one// node with Op=ONAME for a given instance of a variable x.// The same is true for Op=OTYPE and Op=OLITERAL. See Node.mayBeShared.综合上述信息我们得出的结论是,编译器会对stringtoslicebyte的函数调用生成一个AST(笼统语法树)对应的节点。因而我们也知道传递给stringtoslicebyte函数的第一个变量也就对应于上图中的变量a.
其中a的初始值为nodnil()的返回值,即默认为nil. 但是n.Esc == EscNone时,a会变成一个数组。我们看一下EscNone的解释.
// 此代码位于cmd/compile/internal/gc/esc.go中const ( // ... EscNone // Does not escape to heap, result, or parameters. ...)由上可知, EscNone用来判断变量能否逃逸,到这儿了我们就很好办了,接下来我们对现象一和现象二的代码进行逃逸分析.
# 执行变量逃逸分析命令: go run -gcflags '-m -l' test.go# 现象一逃逸分析如下:./test.go:7:14: ([]byte)(a) escapes to heap./test.go:8:13: main ... argument does not escape./test.go:8:13: bs escapes to heap./test.go:8:21: len(bs) escapes to heap./test.go:8:30: cap(bs) escapes to heap[97 98 99] 3 8# 现象二逃逸分析如下:./test.go:7:14: main ([]byte)(a) does not escape./test.go:8:13: main ... argument does not escape./test.go:8:17: len(bs) escapes to heap./test.go:8:26: cap(bs) escapes to heap3 32根据上面的信息我们知道在现象一中,bs变量发生了逃逸,现象二中变量未发生逃逸,也就是说stringtoslicebyte函数的第一个参数在变量未发生逃逸时其值不为nil,变量发生逃逸时其值为nil。到这里我们已经搞明白stringtoslicebyte的第一个参数了, 那我们继续分析stringtoslicebyte的内部逻辑
我们在runtime/string.go中看到stringtoslicebyte第一个参数的类型定义如下:
const tmpStringBufSize = 32type tmpBuf [tmpStringBufSize]byte综上: 现象二中bs变量未发生变量逃逸, stringtoslicebyte第一个参数不为空且是一个长度为32的byte数组, 因而在现象二中生成了一个容量为32的切片
根据对stringtoslicebyte的源码分析, 我们知道现象一调用了rawbyteslice函数
func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size)) p := mallocgc(cap, nil, false) if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return}由上面的代码知道, 切片的容量通过runtime/msize.go中的roundupsize函数计算得出, 其中_MaxSmallSize和class_to_size均定义在runtime/sizeclasses.go
func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) } else { return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]) } } if size+_PageSize < size { return size } return round(size, _PageSize)}因为字符串abc的长度小于_MaxSmallSize(32768),故切片的长度只能取数组class_to_size中的值, 即0, 8, 16, 32, 48, 64, 80, 96, 112, 128....s
至此, 现象一中切片容量为什么为8也真相大白了。相信到这里很多人已经明白现象四和现象五是怎样回事儿了, 其逻辑分别与现象一和现象二是一致的, 有兴趣的, 可以在自己的电脑上面试一试。
字符串直接转切片
那你说了这么多, 现象三还是不能解释啊。请各位看官莫急, 接下来我们继续分析。
相信各位细心的小伙伴应该早就发现了我们在上面的cmd/compile/internal/gc/walk.go源码图中折叠了部分代码, 现在我们就将这块神秘的代码赤裸裸的展现出来
image
我们分析这块代码发现,go编译器在将字符串转字节切片生成AST时,总共分为三步。
先判断该变量能否是常量字符串,假如是常量字符串,则直接通过
types.NewArray创立一个和字符串等长的数组常量字符串生成的切片变量也要进行逃逸分析,并判断其大小能否大于函数栈允许分配给变量的最大长度, 从而判断节点是分配在栈上还是在堆上
最后,假如字符串长度是大于0, 将字符串内容复制到字节切片中, 而后返回。因而现象三中的切片容量是3也就完全清楚了
结论
字符串转字节切片步骤如下
- 判断能否是常量, 假如是常量则转换为等容量等长的字节切片
- 假如是变量, 先判断生成的切片能否发生变量逃逸
- 假如逃逸或者者字符串长度>32, 则根据字符串长度可以计算出不同的容量
- 假如未逃逸且字符串长度<=32, 则字符切片容量为32
扩展
常见逃逸情况
- 函数返回局部指针
- 栈空间不足逃逸
- 动态类型逃逸, 很多函数参数为interface类型,比方fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型, 也会发生逃逸
- 闭包引用对象逃逸
注: 写本文时, 笔者所用go版本为: go1.13.4
参考
https://baijiahao.baidu.com/s?id=1671902920320901161
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 你能一口说出go中字符串转字节切片的容量嘛?