[]byte("")的默认cap是多少

新手上路,请多包涵
s := []byte("")
fmt.Println(cap(s))  //32

but

s := []byte("")
fmt.Println(cap(s))  //0
fmt.Println(s)

Is the compiler does some optimizations?

阅读 3.7k
2 个回答

这个现象确实是编译器优化导致的,我们可以从源码中找出一些证据。

137995 0000000000483200 <main.main>:
……
138009  48323b:      0f 11 44 24 08          movups %xmm0,0x8(%rsp)
138010  483240:      e8 bb a4 fb ff          callq  43d700 <runtime.stringtoslicebyte>
138011  483245:      48 8b 44 24 20          mov    0x20(%rsp),%rax
138012  48324a:      48 8b 4c 24 28          mov    0x28(%rsp),%rcx
……

我们把程序反汇编,如上,可以看到对于 s := []byte("") 这样的语句,编译器会为我们生成 stringtoslicebyte函数进行从string到slice的转换。这个函数定义在:

146 func stringtoslicebyte(buf *tmpBuf, s string) []byte {
147        var b []byte
148        if buf != nil && len(s) <= len(buf) {
149                *buf = tmpBuf{} 
150                b = buf[:len(s)]
151        } else {
152                b = rawbyteslice(len(s))
153        }
154        copy(b, s)
155        return b
156 }
"runtime/string.go" 443 lines --22%--                

tmpBuf定义为一个长度为32的数组。

9 // The constant is known to the compiler.
10 // There is no fundamental theory behind this number.
11 const tmpStringBufSize = 32
12
13 type tmpBuf [tmpStringBufSize]byte
14

当stringtoslicebyte走第一个分支时,从栈上分配内存,如果从栈上分配,就是分配一个长度为32的数组,32是一个写死的和编译器约定好的值。

当stringtoslicebyte走第二个分支时,从堆上分配内存,如果从堆上分配,就是根据字符串的实际长度进行分配。

255 // rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
256 func rawbyteslice(size int) (b []byte) {
257        cap := roundupsize(uintptr(size))
258        p := mallocgc(cap, nil, false)
259        if cap != uintptr(size) {
260                memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
261        }
262
263        *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
264        return
265 }
"runtime/string.go" 443 lines --47%--               

我们接着搜索编译器的源码,可以找到stringtoslicebyte的生成逻辑。

1635        case OSTRARRAYBYTE:
1636                a := nodnil()
1637
1638                if n.Esc == EscNone {
1639                        // Create temporary buffer for slice on stack.
1640                        t := types.NewArray(types.Types[TUINT8], tmpstringbufsize)
1641
1642                        a = nod(OADDR, temp(t), nil)
1643                }
1644
1645                n = mkcall("stringtoslicebyte", n.Type, init, a, conv(n.Left, types.Types[TSTRING]))
"cmd/compile/internal/gc/walk.go" 3928 lines --40%--                                                                   

根据注释看,编译器判断是否在栈上分配的条件,是这个对象是否会逃逸——
编译器会判断一个对象是否会在当前函数外被引用,如果不会就可以通过在当前栈上分配该对象,无需GC处理,达到优化的目的。
这个过程属于编译器逃逸分析(优化)的一部分。逃逸分析的相关源码在:
"cmd/compile/internal/gc/esc.go"

通过如下命令可以获取到编译器逃逸分析的结果

没有fmt.Println(s)时的逃逸分析结果:

$ go tool compile -m test.go
test.go:7:16: cap(s) escapes to heap
test.go:7:24: len(s) escapes to heap
test.go:6:12: main ([]byte)("") does not escape
test.go:7:12: main ... argument does not escape

有fmt.Println(s)时的逃逸分析结果:

$ go tool compile -m test.go
test.go:7:16: cap(s) escapes to heap
test.go:7:24: len(s) escapes to heap
test.go:8:12: s escapes to heap
test.go:6:12: ([]byte)("") escapes to heap
test.go:7:12: main ... argument does not escape
test.go:8:12: main ... argument does not escape

以上,基本上就解释了我们所看到的现象。

这个问题很有意思,从结果上来看,编译器肯定是对栈上分配的临时数组变量做了优化(临时分配的固定大小的容量),若一但操作了该数组变量,那么会根据实际使用情况更新数组变量的容量大小。

此外,如果是将该数组定义成包内部变量或者全局变量,这个cap的结果也会是0,因为它会在堆上进行分配。

我也仅仅是根据测试结果进行的猜测,具体内部实现细节可能需要大牛来科普以下。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏