Go語言模型:string的底層數據結構與高效操作詳解
Golang的string類型底層數據結構簡單,本質也是一個結構體實例,且是const不可變。
string的底層數據結構
通過下面一個例子來看:
package main import ( "fmt" "unsafe" ) // from: string.go 在GoLand IDE中雙擊shift快速找到 type stringStruct struct { array unsafe.Pointer // 指向一個 [len]byte 的數組 length int // 長度 } func main() { test := "hello" p := (*str)(unsafe.Pointer(&test)) fmt.Println(&p, p) // 0xc420070018 &{0xa3f71 5} c := make([]byte, p.length) for i := 0; i < p.length; i++ { tmp := uintptr(unsafe.Pointer(p.array)) // 指針類型轉換通過unsafe包 c[i] = *(*byte)(unsafe.Pointer(tmp + uintptr(i))) // 指針運算隻能通過uintptr } fmt.Println(c) // [104 101 108 108 111] fmt.Println(string(c)) // [byte] --> string, "hello" test2 := test + " world" // 字符串是不可變類型,會生成一個新的string實例 p2 := (*str)(unsafe.Pointer(&test2)) fmt.Println(&p2, p2) // 0xc420028030 &{0xc42000a2e5 11} fmt.Println(test2) // hello, world }
string的拼接與修改
+操作
string類型是一個不可變類型,那麼任何對string的修改都會新生成一個string的實例,如果是考慮效率的場景就要好好考慮一下如何修改瞭。先說一下最長用的+操作,同樣上面的例子,看一下+操作拼接字符串的反匯編:
25 test2 := test + " world" 0x00000000004824d7 <+1127>: lea 0x105a2(%rip),%rax # 0x492a80 0x00000000004824de <+1134>: mov %rax,(%rsp) 0x00000000004824e2 <+1138>: callq 0x40dda0 <runtime.newobject> # 調用newobject函數 0x00000000004824e7 <+1143>: mov 0x8(%rsp),%rax 0x00000000004824ec <+1148>: mov %rax,0xa0(%rsp) 0x00000000004824f4 <+1156>: mov 0xa8(%rsp),%rax 0x00000000004824fc <+1164>: mov 0x8(%rax),%rcx 0x0000000000482500 <+1168>: mov (%rax),%rax 0x0000000000482503 <+1171>: mov %rax,0x8(%rsp) 0x0000000000482508 <+1176>: mov %rcx,0x10(%rsp) 0x000000000048250d <+1181>: movq $0x0,(%rsp) 0x0000000000482515 <+1189>: lea 0x30060(%rip),%rax # 0x4b257c 0x000000000048251c <+1196>: mov %rax,0x18(%rsp) 0x0000000000482521 <+1201>: movq $0x6,0x20(%rsp) 0x000000000048252a <+1210>: callq 0x43cc00 <runtime.concatstring2> # 調用concatstring2函數
因為當前go[2018.11 version: go1.11]的不是遵循默認的x86 calling convention用寄存器傳參,而是通過stack進行傳參,所以go的反匯編不像c的那麼容易理解,不過大概看懂+背後的操作還是沒問題的,看一下runtime源碼的拼接函數:
func concatstring2(buf *tmpBuf, a [2]string) string { return concatstrings(buf, a[:]) } // concatstrings implements a Go string concatenation x+y+z+... // The operands are passed in the slice a. // If buf != nil, the compiler has determined that the result does not // escape the calling function, so the string data can be stored in buf // if small enough. func concatstrings(buf *tmpBuf, a []string) string { idx := 0 l := 0 count := 0 for i, x := range a { n := len(x) if n == 0 { continue } if l+n < l { throw("string concatenation too long") } l += n count++ idx = i } if count == 0 { return "" } // If there is just one string and either it is not on the stack // or our result does not escape the calling frame (buf != nil), // then we can return that string directly. if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) { return a[idx] } s, b := rawstringtmp(buf, l) for _, x := range a { copy(b, x) // 最關鍵的拷貝操作 b = b[len(x):] } return s }
分析runtime的concatstrings實現,可以看出+最後新申請buf,拷貝原來的string到buf,最後返回新實例。那麼每次的+操作,都會涉及新申請buf,然後是對應的copy。如果反復使用+,就不可避免有大量的申請內存操作,對於大量的拼接,性能就會受到影響瞭。
bytes.Buffer
通過看源碼,bytes.Buffer 增長buffer時是按照2倍來增長內存,可以有效避免頻繁的申請內存,通過一個例子來看:
func main() { var buf bytes.Buffer for i := 0; i < 10; i++ { buf.WriteString("hi ") } fmt.Println(buf.String()) }
對應的byte包庫函數源碼
// @file: buffer.go func (b *Buffer) WriteString(s string) (n int, err error) { b.lastRead = opInvalid m, ok := b.tryGrowByReslice(len(s)) if !ok { m = b.grow(len(s)) // 高效的增長策略 -> let capacity get twice as large } return copy(b.buf[m:], s), nil } // @file: buffer.go // let capacity get twice as large !!! func (b *Buffer) grow(n int) int { m := b.Len() // If buffer is empty, reset to recover space. if m == 0 && b.off != 0 { b.Reset() } // Try to grow by means of a reslice. if i, ok := b.tryGrowByReslice(n); ok { return i } // Check if we can make use of bootstrap array. if b.buf == nil && n <= len(b.bootstrap) { b.buf = b.bootstrap[:n] return 0 } c := cap(b.buf) if n <= c/2-m { // We can slide things down instead of allocating a new // slice. We only need m+n <= c to slide, but // we instead let capacity get twice as large so we // don't spend all our time copying. copy(b.buf, b.buf[b.off:]) } else if c > maxInt-c-n { panic(ErrTooLarge) } else { // Not enough space anywhere, we need to allocate. buf := makeSlice(2*c + n) copy(buf, b.buf[b.off:]) b.buf = buf } // Restore b.off and len(b.buf). b.off = 0 b.buf = b.buf[:m+n] return m }
string.join
這個函數可以一次申請最終string的大小,但是使用得預先準備好所有string,這種場景也是高效的,一個例子:
func main() { var strs []string for i := 0; i < 10; i++ { strs = append(strs, "hi") } fmt.Println(strings.Join(strs, " ")) }
對應庫的源碼:
// Join concatenates the elements of a to create a single string. The separator string // sep is placed between elements in the resulting string. func Join(a []string, sep string) string { switch len(a) { case 0: return "" case 1: return a[0] case 2: // Special case for common small values. // Remove if golang.org/issue/6714 is fixed return a[0] + sep + a[1] case 3: // Special case for common small values. // Remove if golang.org/issue/6714 is fixed return a[0] + sep + a[1] + sep + a[2] } // 計算好最終的string的大小 n := len(sep) * (len(a) - 1) // for i := 0; i < len(a); i++ { n += len(a[i]) } b := make([]byte, n) bp := copy(b, a[0]) for _, s := range a[1:] { bp += copy(b[bp:], sep) bp += copy(b[bp:], s) } return string(b) }
strings.Builder (go1.10+)
看到這個名字,就想到瞭Java的庫,哈哈,這個Builder用起來是最方便的,不過是在1.10後引入的。其高效也是體現在2倍速的內存增長, WriteString函數利用瞭slice類型對應append函數的2倍速增長。
一個例子:
func main() { var s strings.Builder for i := 0; i < 10; i++ { s.WriteString("hi ") } fmt.Println(s.String()) }
對應庫的源碼
@file: builder.go // WriteString appends the contents of s to b's buffer. // It returns the length of s and a nil error. func (b *Builder) WriteString(s string) (int, error) { b.copyCheck() b.buf = append(b.buf, s...) return len(s), nil }
總結
Golang的字符串處理還是挺方便的,有垃圾回收和一些內置的語言級寫法支持,讓復雜字符串操作沒有那麼繁瑣瞭,比起C/C++高效瞭不少。
補充:go string的內部實現
go string 內部實現
這個string的探索
來來個例子
func boo(a int, b int)(int, string){ return a + b, "abcd" }
81079 000000000044dfa0 <main.boo>: 81080 44dfa0:>------48 c7 44 24 18 00 00 >--movq $0x0,0x18(%rsp) 81081 44dfa7:>------00 00- 81082 44dfa9:>------0f 57 c0 >--xorps %xmm0,%xmm0 81083 44dfac:>------0f 11 44 24 20 >--movups %xmm0,0x20(%rsp) 81084 44dfb1:>------48 8b 44 24 08 >--mov 0x8(%rsp),%rax 81085 44dfb6:>------48 03 44 24 10 >--add 0x10(%rsp),%rax 81086 44dfbb:>------48 89 44 24 18 >--mov %rax,0x18(%rsp) 81087 44dfc0:>------48 8d 05 d4 eb 01 00 >--lea 0x1ebd4(%rip),%rax # 46cb9b <go.string.*+0xbb> 81088 44dfc7:>------48 89 44 24 20 >--mov %rax,0x20(%rsp) 81089 44dfcc:>------48 c7 44 24 28 04 00 >--movq $0x4,0x28(%rsp) 81090 44dfd3:>------00 00- 81091 44dfd5:>------c3 >--retq---
其中
81087 44dfc0:>------48 8d 05 d4 eb 01 00 >--lea 0x1ebd4(%rip),%rax # 46cb9b <go.string.*+0xbb> 81088 44dfc7:>------48 89 44 24 20 >--mov %rax,0x20(%rsp) 81089 44dfcc:>------48 c7 44 24 28 04 00 >--movq $0x4,0x28(%rsp) 81090 44dfd3:>------00 00- 81091 44dfd5:>------c3 >--retq--- lea 0x1ebd4(%rip),%rax得到char*, mov %rax,0x20(%rsp)復制給返回值, movq $0x4,0x28(%rsp)把長度也填進去,
其實可以看到string就是c裡面的char* 和len的組合
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。
推薦閱讀:
- Go語言如何高效的進行字符串拼接(6種方式對比分析)
- Golang語言如何高效拼接字符串詳解
- go 迭代string數組操作 go for string[]
- Go語言中的字符串拼接方法詳情
- Go中string與[]byte高效互轉的方法實例