Go Go string to slice 議題

日前在看 Go runtime/string.go source code 的時候,意外看到一篇文章 Go中string转[]byte的陷阱,不過這篇文章的日期間隔已久,Go version 也從文章中的 1.10 改版到 1.14 了,所以想要更新一下這個議題現況。

hugo

Version

Go version: 1.14

問題摘要

1. runtime.stringtoslicebyte

首先,文章中提到的例子:

1func main() {
2    s := []byte("")
3    s1 := append(s, 'a')
4    s2 := append(s, 'b')
5    //fmt.Println(s1, "==========", s2)
6    fmt.Println(string(s1), "==========", string(s2))
7}

在沒有 fmt.Println(s1, "==========", s2) 這一行的情況下,output 會產生:

1b ========== b

的結果。

反之,如果加了 fmt.Println(s1, "==========", s2) ,由於 Escape Analysis 的關係,因此結果會變成:

1a ========== b

此陷阱是由於在一開始宣告 byte slice 時,是由 string 轉換成 slice

1s := []byte("")

這樣的寫法會觸發 runtime.stringtoslicebyte ,細究此 function:

 1func stringtoslicebyte(buf *tmpBuf, s string) []byte {
 2	var b []byte
 3	if buf != nil && len(s) <= len(buf) {
 4		*buf = tmpBuf{}
 5		b = buf[:len(s)]
 6	} else {
 7		b = rawbyteslice(len(s))
 8	}
 9	copy(b, s)
10	return b
11}

會發現 escape analysis 結果 (buf != nil) 有可能影響最後回傳的 []byte slice,進而導致後續 slice append 行為不同。

2. byte slice initialization

另外,在 issue cmd/compile: constant string -> []byte and []byte -> string conversions aren’t constant folded 中也提到 byte 的行為不一致:

1a := []byte("foo")
2b := []byte{'f','o','o'}

因為前者 a 會觸發 runtime.stringtoslicebyte 的關係,所以 []byte("foo") 比起 []byte{'f','o','o'} 需要額外的執行成本,但是對使用者來說, []byte("foo") 應該是更常見的寫法。

Optimization

因此,在 cmd/compile: make []byte("…") more efficient 這項 commit 就針對

[]byte(string) conversions

行為做改進,不再使用 stringtoslicebyte ,而是定義一個與 string 相同長度的 byte array (記憶體空間根據 escape analysis 可能在 heap 或是 goroutine stack),並且使用 string 來將此 array 初始化,再轉換成 slice 回傳。

實作方式可參閱下面的 asm code,可以看到已和陷阱文章中所提到的 asm code 有明顯差異。

 1// s := []byte("abc")
 2
 30x0021 00033	PCDATA	$0, $1
 40x0021 00033	PCDATA	$1, $0
 50x0021 00033	LEAQ	type.[3]uint8(SB), AX
 60x0028 00040	PCDATA	$0, $0
 70x0028 00040	MOVQ	AX, (SP)
 80x002c 00044	CALL	runtime.newobject(SB)
 90x0031 00049	PCDATA	$0, $1
100x0031 00049	MOVQ	8(SP), AX
110x0036 00054	MOVW	$25185, (AX)
120x003b 00059	MOVB	$99, 2(AX)

這樣的更動自然地也就消除了一開始所提到的 string 轉 byte slice 陷阱,自 Go 1.12 版本之後,[]byte(string) 會回傳 cap, len 值皆為 string 長度的 byte slice,而不會受到 runtime.stringtoslicebyte 邏輯因素而產生 cap 為 32 的 slice (具體內容可詳見Go中string转[]byte的陷阱)。

總結

由於改善 []byte(string) 的 compile 結果,也消除了之前可能會遇到的陷阱,這其中從發現問題到修改過程都有值得讓人好好學習的知識和概念。