来自公众号:新世界杂货铺
神奇的景象
切片,切片,又是切片!
前一篇文章讲的是切片,明天遇到的神奇问题还是和切片无关,具体怎么个神奇法,咱们来看看上面几个景象
景象一
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
剖析
到这儿我曾经满脑子问号了
字符串变量转切片
一个小小的字符串转切片,外部到底产生了什么,居然如此的神奇。这种时候只好祭出前一篇文章的套路了,看看汇编代码 ( 心愿之后有机会可能对 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" 放入寄存器 AX
0x0036 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" 放入寄存器 AX
0x0036 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, $2
0x0047 00071 (test.go:7) LEAQ ""..autotmp_5+64(SP), CX // 将外部变量 autotmp_5 放入寄存器 CX
0x004c 00076 (test.go:7) PCDATA $0, $1
0x004c 00076 (test.go:7) MOVQ CX, (SP) // 将 CX 中的内容传递给 runtime.stringtoslicebyte(SB)的第一个参数
0x0050 00080 (test.go:7) PCDATA $0, $0
0x0050 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 发现了如下代码块
咱们查看 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 heap
3 32
依据下面的信息咱们晓得在景象一中,bs 变量产生了逃逸,景象二中变量未产生逃逸,也就是说 stringtoslicebyte 函数的第一个参数在变量未产生逃逸时其值不为 nil, 变量产生逃逸时其值为 nil。到这里咱们曾经搞明确 stringtoslicebyte 的第一个参数了,那咱们持续剖析 stringtoslicebyte 的外部逻辑
咱们在 runtime/string.go 中看到 stringtoslicebyte 第一个参数的类型定义如下:
const tmpStringBufSize = 32
type 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
源码图中折叠了局部代码,当初咱们就将这块神秘的代码赤裸裸的展现进去
咱们剖析这块代码发现,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 的技术摸索
原创不易,低微求关注珍藏二连.