Go內(nèi)存中的字符串操作
內(nèi)存中的字符串類型詳細(xì)描述了字符串在內(nèi)存中的結(jié)構(gòu)及其類型信息。
本文主要研究字符串的各種操作(語(yǔ)法糖),在內(nèi)存中實(shí)際的樣子。
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
聲明
操作系統(tǒng)、處理器架構(gòu)、Go版本不同,均有可能造成相同的源碼編譯后運(yùn)行時(shí)的寄存器值、內(nèi)存地址、數(shù)據(jù)結(jié)構(gòu)不同。
本文僅保證學(xué)習(xí)過程中的分析數(shù)據(jù)在當(dāng)前環(huán)境下的準(zhǔn)確有效性。
操作類型
比較
- 相等性比較
- 不等性比較
連接(相加)
與[]byte的轉(zhuǎn)換
與[]byte的拷貝
代碼清單
- package main
- import (
- "fmt"
- )
- func main() {
- var array [20]byte
- var s = "copy hello world"
- string2slice(s)
- copyString(array[:], s)
- slice2string(array[:])
- compare()
- concat()
- }
- //go:noinline
- func copyString(slice []byte, s string) {
- copy(slice, s)
- PrintSlice(slice)
- }
- //go:noinline
- func string2slice(s string) {
- PrintSlice([]byte(s))
- }
- //go:noinline
- func slice2string(slice []byte) {
- PrintString(string(slice))
- }
- //go:noinline
- func compare() {
- var h = "hello"
- var w = "world!"
- PrintBool(h > w)
- PrintBool(h < w)
- PrintBool(h >= w)
- PrintBool(h <= w)
- PrintBool(h != w) // PrintBool(true)
- PrintBool(h == w) // PrintBool(false)
- PrintBool(testEqual(h, w))
- PrintBool(testNotEqual(h, w))
- }
- //go:noinline
- func testEqual(h, w string) bool {
- return h == w
- }
- //go:noinline
- func testNotEqual(h, w string) bool {
- return h != w
- }
- //go:noinline
- func concat() {
- hello := "hello "
- world := "world"
- jack := "Jack"
- rose := " Rose "
- lucy := "Lucy"
- lily := " Lily "
- ex := "!"
- PrintString(concat2(hello, world))
- PrintString(concat3(hello, jack, ex))
- PrintString(concat4(hello, jack, rose, ex))
- PrintString(concat5(hello, jack, rose, lucy, lily))
- PrintString(concat6(hello, jack, rose, lucy, lily, ex))
- }
- //go:noinline
- func concat2(a, b string) string {
- return a + b
- }
- //go:noinline
- func concat3(a, b, c string) string {
- return a + b + c
- }
- //go:noinline
- func concat4(a, b, c, d string) string {
- return a + b + c + d
- }
- //go:noinline
- func concat5(a, b, c, d, e string) string {
- return a + b + c + d + e
- }
- //go:noinline
- func concat6(a, b, c, d, e, f string) string {
- return a + b + c + d + e + f
- }
- //go:noinline
- func PrintBool(v bool) {
- fmt.Println("v =", v)
- }
- //go:noinline
- func PrintString(v string) {
- fmt.Println("s =", v)
- }
- //go:noinline
- func PrintSlice(s []byte) {
- fmt.Println("slice =", s)
- }
- 添加go:noinline注解避免內(nèi)聯(lián),方便指令分析
- 定義PrintBool/PrintSlice/PrintString函數(shù)避免編譯器插入runtime.convT*函數(shù)調(diào)用
深入內(nèi)存
字符串轉(zhuǎn)[]byte
代碼清單中的string2slice函數(shù)代碼非常簡(jiǎn)單,用于觀察[]byte(s)具體實(shí)現(xiàn)邏輯,編譯之后指令如下:

可以清晰地看到,我們?cè)诖a中的[]byte(s),被Go編譯器替換為runtime.stringtoslicebyte函數(shù)調(diào)用。
runtime.stringtoslicebyte函數(shù)定義在runtime/string.go源碼文件中,Go編譯器傳遞給該函數(shù)的buf參數(shù)值為nil。
- 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
- }
rawbyteslice函數(shù)的功能是申請(qǐng)一塊內(nèi)存用于存儲(chǔ)拷貝后的數(shù)據(jù)。
[]byte轉(zhuǎn)字符串
代碼清單中的slice2string函數(shù)代碼非常簡(jiǎn)單,用于觀察string(slice)具體實(shí)現(xiàn)邏輯,編譯之后指令如下:
可以清晰地看到,我們?cè)诖a中的string(slice),被Go編譯器替換為runtime.slicebytetostring函數(shù)調(diào)用。
runtime.slicebytetostring函數(shù)定義在runtime/string.go源碼文件中,Go編譯器傳遞給該函數(shù)的buf參數(shù)值為nil。
拷貝字符串到[]byte
代碼清單中的copyString函數(shù)代碼非常簡(jiǎn)單,用于觀察copy(slice, s)具體實(shí)現(xiàn)邏輯,編譯之后指令如下:
這個(gè)邏輯稍微復(fù)雜一點(diǎn)點(diǎn),將以上指令再次翻譯為Go偽代碼如下:
- func copyString(slice reflect.SliceHeader, s reflect.StringHeader) {
- n := slice.Len
- if slice.Len > s.Len {
- n = s.Len
- }
- if slice.Data != s.Data {
- runtime.memmove(slice.Data, s.Data, n)
- }
- PrintSlice(*(*[]byte)(unsafe.Pointer(&slice)))
- }
可以看到,Go編譯器在copy(slice, s)這個(gè)簡(jiǎn)單易用語(yǔ)法糖背后做了很多的工作。
經(jīng)過比較,以上偽代碼與runtime/slice.go源碼文件中的slicecopy函數(shù)非常相似,但又不完全一致。
不等性比較
代碼清單中的compare函數(shù)測(cè)試了兩個(gè)字符串的各種比較操作。
查看該函數(shù)的指令,發(fā)現(xiàn)Go編譯器將以下四種比較操作全部轉(zhuǎn)換為runtime.cmpstring函數(shù)調(diào)用:
- >
- <
- >=
- <=
runtime.cmpstring函數(shù)是一個(gè)編譯器函數(shù),不會(huì)被直接調(diào)用,聲明在cmd/compile/internal/gc/builtin/runtime.go源碼文件中,由匯編語(yǔ)言實(shí)現(xiàn)。
GOARCH=amd64的實(shí)現(xiàn)位于internal/bytealg/compare_amd64.s源碼文件中。
該函數(shù)返回值可能是:
然后使用cmp匯編指令將返回值與0進(jìn)行比較,再使用以下匯編指令保存最終的比較結(jié)果(true / false):
在本例中,有兩個(gè)特殊的比較,分別被編譯為單條指令:
- h != w 被編譯為 movb $0x1,(%rsp)
- h == w 被編譯為 movb $0x0,(%rsp)
這是因?yàn)樵诒纠芯幾g器知道"hello"與"world"兩個(gè)字符串不相等,所以直接在編譯的時(shí)候直接把比較結(jié)果編譯到機(jī)器指令中。
所以,在代碼定義了testEqual和testNotEqual函數(shù)用于比較字符串變量。
相等性比較
關(guān)于相等性比較,在 內(nèi)存中的字符串類型 中已經(jīng)做了非常詳細(xì)的分析和說(shuō)明。
在本文的代碼清單中,testEqual函數(shù)指令如下,與runtime.strequal函數(shù)一致,是因?yàn)榫幾g器將runtime.strequal函數(shù)內(nèi)聯(lián)(inline)到了testEqual函數(shù)中。
出乎意料的是,!=與==編譯后的幾乎一致,只是兩處指令對(duì)結(jié)果進(jìn)行了相反的操作:
字符串連接(相加)
在本文的代碼清單中,concat函數(shù)用于觀察字符串的連接(+)操作,測(cè)試結(jié)果表明:
- 2個(gè)字符串相加,實(shí)際調(diào)用runtime.concatstring2函數(shù)
- 3個(gè)字符串相加,實(shí)際調(diào)用runtime.concatstring3函數(shù)
- 4個(gè)字符串相加,實(shí)際調(diào)用runtime.concatstring4函數(shù)
- 5個(gè)字符串相加,實(shí)際調(diào)用runtime.concatstring5函數(shù)
- 超過5個(gè)字符串相加,實(shí)際調(diào)用runtime.concatstrings函數(shù)
以上這些函數(shù)調(diào)用,都是Go編譯器的代碼生成和插入工作。
在插入runtime.concatstring*函數(shù)的過程中,編譯器傳遞給這些函數(shù)的buf參數(shù)的值為nil。
runtime.concatstring*函數(shù)的實(shí)現(xiàn)非常簡(jiǎn)單,這里不再進(jìn)一步贅述。
小結(jié)
從以上詳細(xì)的分析可以看到,我們?cè)陂_發(fā)過程中,所有對(duì)字符串進(jìn)行的簡(jiǎn)單操作,都會(huì)被Go編譯器編碼為復(fù)雜的指令和函數(shù)調(diào)用。
許多開發(fā)者喜歡使用Go進(jìn)行開發(fā),理由是Go語(yǔ)言非常簡(jiǎn)單、簡(jiǎn)潔。
是的,我們都喜歡這種甜甜的語(yǔ)法糖。
而且,發(fā)掘語(yǔ)法糖背后的秘密,也是很好玩的事。
本文轉(zhuǎn)載自微信公眾號(hào)「Golang In Memory」