【Go】string 優化誤區及建議
原文連結: https://blog.thinkeridea.com/...
本文原標題為 《string 也是引用型別》,經過 郝林 大佬指點原標題存在誘導性,這裡解釋一下 "引用型別" 有兩個特徵:1、多個變數引用一塊記憶體資料,不建立變數的副本,2、修改任意變數的資料,其它變數可見。顯然字串只滿足了 "引用型別" 的第一個特點,不能滿足第二個特點,顧不能說字串是引用型別,感謝大佬指正。
初學 Go
語言的朋友總會在傳 []byte
和 string
之間有著很多糾結,實際上是沒有了解 string
與 slice
的本質,而且讀了一些程式原始碼,也發現很多與之相關的問題,下面類似的程式碼估計很多初學者都寫過,也充分說明了作者當時內心的糾結:
package main
import "bytes"
func xx(s []byte) []byte{
....
return s
}
func main(){
s := "xxx"
s = string(xx([]byte(s)))
s = string(bytes.Replace([]byte(s), []byte("x"), []byte(""), -1))
}
雖然這樣的程式碼並不是來自真實的專案,但是確實有人這樣設計,單從設計上看就很糟糕了,這樣設計的原因很多人說:“slice
是引用型別,傳遞引用型別效率高呀”,主要原因不瞭解兩者的本質。
上面這個例子如果覺得有點基礎和可愛,下面這個例子貌似並不那麼容易說明其存在的問題了吧。
package main
func xx(s *string) *string{
....
return s
}
func main(){
s := "xx"
s = *xx(&s)
ss :=[]*string{}
ss = append(ss, &s)
}
指標效率高,我就用指標多好,可以減少記憶體分配呀,設計函式都接收指標變數,程式效能會有很大提升,在實際的專案中這種例子也不少見,我想通過這篇文件來幫助初學者走出誤區,減少適得其反的優化技巧。
slice 的定義
在之前 “【Go】深入剖析 slice 和 array” 一文中說了 slice
在記憶體中的儲存模式,slice
本身包含一個指向底層陣列的指標,一個 int
型別的長度和一個 int
型別的容量, 這就是 slice
的本質, []byte
本身也是一個 slice
,只是底層陣列儲存的元素是 byte
。下面這個圖就是 slice
的在記憶體中的狀態:
看一下 reflect.SliceHeader
如何定義 slice
在記憶體中的結構吧:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
slice
是引用型別是 slice
本身會包含一個地址,在傳遞 slice
時只需要分配 SliceHeader
就好了, 而 SliceHeader
只包含了三個 int
型別,相當於傳遞一個 slice
就只需要拷貝 SliceHeader
,而不用拷貝整個底層陣列,所以才說 slice
是引用型別的。
那麼字串呢,計算機中我們處理的大多數問題都和字串有關,難道傳遞字串真的需要那麼高的成本,需要藉助 slice
和指標來減少記憶體開銷嗎。
string 的定義
reflect
包裡面也定義了一個 StringHeader
看一下吧:
type StringHeader struct {
Data uintptr
Len int
}
字串只包含了兩個 int
型別的資料,其中一個是指標,一個是字串的長度,從 StringHeader
定義來看 string
並不會發生拷貝的,傳遞 string
只會拷貝 StringHeader
而已。
藉助 unsafe
來分析一下情況是不是這樣吧:
package main
import (
"reflect"
"unsafe"
"github.com/davecgh/go-spew/spew"
)
func xx(s string) {
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
spew.Dump(sh)
}
func main() {
s := "xx"
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
spew.Dump(sh)
xx(s)
xx(s[:1])
xx(s[1:])
}
上面這段程式碼的輸出如下:
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ee0,
Len: (int) 2
}
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ee0,
Len: (int) 2
}
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ee0,
Len: (int) 1
}
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ee1,
Len: (int) 1
}
可以發現前三個輸出的指標都是同一個地址,第四個的地址發生了一個位元組的偏移,分析來看傳遞字串確實沒有分配新的記憶體,同時和 slice
一樣即使傳遞字串的子串也不會分配新的記憶體空間,而是指向原字串的中的一個位置。
這樣說來把 string
轉成 []byte
還浪費的一個 int
的空間呢,需要分配更多的記憶體,真是適得其反呀,而且型別轉換會發生記憶體拷貝,從 string
轉為 []byte
才是真的把 string
底層資料全部拷貝一遍呢,真是得不償失呀。
string 的兩個小特性
字串還有兩個小特性,針對字面量 (就是直接寫在程式中的字串),會建立在只讀空間上,並且被複用,看一下下面的一個小例子:
package main
import (
"reflect"
"unsafe"
"github.com/davecgh/go-spew/spew"
)
func main() {
a := "xx"
b := "xx"
c := "xxx"
spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&a)))
spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&b)))
spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&c)))
}
從輸出可以瞭解到,相同的字面量會被複用,但是子串是不會複用空間的,這就是編譯器給我們帶來的福利了,可以減少字面量字串佔用的記憶體空間。
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ea0,
Len: (int) 2
}
(reflect.StringHeader) {
Data: (uintptr) 0x10f5ea0,
Len: (int) 2
}
(reflect.StringHeader) {
Data: (uintptr) 0x10f5f2e,
Len: (int) 3
}
另一個小特性大家都知道,就是字串是不能修改的,如果我們不希望呼叫函式修改我們的資料,最好傳遞字串,高效有安全。
不過有了 unsafe
這個黑魔法,字串的這一個特性也就不那麼可靠了。
package main
import (
"fmt"
"reflect"
"strings"
"unsafe"
)
func main() {
a := strings.Repeat("x", 10)
fmt.Println(a)
strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&a))
sliceHeader := reflect.SliceHeader{
Data: strHeader.Data,
Len: strHeader.Len,
Cap: strHeader.Len,
}
b := *(*[]byte)(unsafe.Pointer(&sliceHeader))
b[1] = 'a'
fmt.Println(a)
}
從輸出裡面居然發現字串被修改了, 我們沒有辦法直接修改字串,但是可以利用 slice
和 string
本身結構的特性,建立一個 slice
讓它的指標指向 string
的指標位置,然後藉助 unsafe
把這個 SliceHeader
轉成 []byte
來修改字串,字串確實被修改了。
xxxxxxxxxx
xaxxxxxxxx
看了上面的例子是不是開始擔心把字串傳給其它函式真的不會更改嗎?感覺很不放心的樣子,難道使用任何函式都要了解它的內部實現嗎,其實這種情況極少發生,還記得之前說的那個字串特性嗎,字面量字串會放到只讀空間中,這個很重要,可以保證不是任何函式想修改我們的字串就可以修改的。
package main
import (
"reflect"
"unsafe"
)
func main() {
defer func() {
recover()
}()
a := "xx"
strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&a))
sliceHeader := reflect.SliceHeader{
Data: strHeader.Data,
Len: strHeader.Len,
Cap: strHeader.Len,
}
b := *(*[]byte)(unsafe.Pointer(&sliceHeader))
b[1] = 'a'
}
執行上面的程式碼發生了一個執行時不可修復的錯誤,就是這個特性其它函式不能確保輸入字串是否是字面量,也是不會惡意修改我們字串的了。
unexpected fault address 0x1095dd5
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x1095dd5 pc=0x106c804]
goroutine 1 [running]:
runtime.throw(0x1095fde, 0x5)
/usr/local/go/src/runtime/panic.go:608 +0x72 fp=0xc000040700 sp=0xc0000406d0 pc=0x10248d2
runtime.sigpanic()
/usr/local/go/src/runtime/signal_unix.go:387 +0x2d7 fp=0xc000040750 sp=0xc000040700 pc=0x1037677
main.main()
/Users/qiyin/project/go/src/github.com/yumimobi/test/a.go:22 +0x84 fp=0xc000040798 sp=0xc000040750 pc=0x106c804
runtime.main()
/usr/local/go/src/runtime/proc.go:201 +0x207 fp=0xc0000407e0 sp=0xc000040798 pc=0x1026247
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0000407e8 sp=0xc0000407e0 pc=0x104da51
關於字串轉 []byte
在 go-extend 擴充套件包中有直接的實現,這種用法在 go-extend 內部方法實現中也有大量使用, 實際上因為原資料型別和處理資料的函式型別不一致,使用這種方法轉換字串和 []byte
可以極大的提升程式效能
-
exbytes.ToString 零成本的把
[]byte
轉為string
。 -
exstrings.UnsafeToBytes 零成本的把
[]byte
轉為string
。
上面這兩個函式用的好,可以極大的提升我們程式的效能,關於 exstrings.UnsafeToBytes
我們轉換不確定是否是字面量的字串時就需要確保呼叫的函式不會修改我們的資料,這往常在呼叫 bytes
裡面的方法十分有效。
傳字串和字串指標的區別
之前分析了傳遞 slice
並沒有 string
高效,何況轉換資料型別本身就會發生資料拷貝。
那麼在這篇文章的第二個例子,為什麼說傳遞字串指標也不好呢,要了解指標在底層就是一個 int
型別的資料,而我們字串只是兩個 int
而已,另外如果瞭解 GC
的話,GC
只處理堆上的資料,傳遞指標字串會導致資料逃逸到堆上,閱讀標準庫的程式碼會有很多註釋說明避免逃逸到堆上,這樣會極大的增加 GC
的開銷,GC
的成本可謂是很高的呀。
疑惑
這篇文章說 “傳遞 slice
並沒有 string
高效”,為什麼還會有 bytes
包的存在呢,其中很多函式的功能和 strings
包的功能一致,只是把 string
換成了 []byte
, 既然傳遞 []byte
沒有 string
效率好,這個包存在的意義是什麼呢。
我們想一下轉換資料型別是會發生資料拷貝,這個成本可是大的多呀,如果我們資料本身就是 []byte
型別,使用 strings
包就需要轉換資料型別了。
另外我們對比兩個函式來看下一下即使傳遞 []byte
沒有 string
效率好,但是標準庫實現上卻會導致兩個函式有很大的效能差異的。
strings.Repeat
函式:
func Repeat(s string, count int) string {
// Since we cannot return an error on overflow,
// we should panic if the repeat will generate
// an overflow.
// See Issue golang.org/issue/16237
if count < 0 {
panic("strings: negative Repeat count")
} else if count > 0 && len(s)*count/count != len(s) {
panic("strings: Repeat count causes overflow")
}
b := make([]byte, len(s)*count)
bp := copy(b, s)
for bp < len(b) {
copy(b[bp:], b[:bp])
bp *= 2
}
return string(b)
}
bytes.Repeat
函式:
func Repeat(b []byte, count int) []byte {
// Since we cannot return an error on overflow,
// we should panic if the repeat will generate
// an overflow.
// See Issue golang.org/issue/16237.
if count < 0 {
panic("bytes: negative Repeat count")
} else if count > 0 && len(b)*count/count != len(b) {
panic("bytes: Repeat count causes overflow")
}
nb := make([]byte, len(b)*count)
bp := copy(nb, b)
for bp < len(nb) {
copy(nb[bp:], nb[:bp])
bp *= 2
}
return nb
}
上面兩個函式的實現非常相似,除了型別不同 strings
包在處理完資料發生了一次型別轉換,使用 bytes
只有一次記憶體分配,而 strings
是兩次。
我們可以藉助 exbytes.ToString 函式把 bytes.Repeat
的返回沒有任何成本的轉換會我們需要的字串,如果我們輸入也是一個字串的話,還可以藉助 exstrings.UnsafeToBytes 來轉換輸入的資料型別。
例如:
s := exbytes.ToString(bytes.Repeat(exstrings.UnsafeToBytes("x"), 10))
不過這樣寫有點太麻煩了,實際上 exstrings 包裡面正在修改 strings
裡面一些類似函式的問題,所有的實現基本和標準庫一致,只是把其中型別轉換的部分用 exbytes.ToString 優化了一下,可以提升效能,也能提升開發效率。
func UnsafeRepeat(s string, count int) string {
// Since we cannot return an error on overflow,
// we should panic if the repeat will generate
// an overflow.
// See Issue golang.org/issue/16237
if count < 0 {
panic("strings: negative Repeat count")
} else if count > 0 && len(s)*count/count != len(s) {
panic("strings: Repeat count causes overflow")
}
b := make([]byte, len(s)*count)
bp := copy(b, s)
for bp < len(b) {
copy(b[bp:], b[:bp])
bp *= 2
}
return exbytes.ToString(b)
}
如果用上面的函式只需要下面這樣寫就可以了:
s:=exstrings.UnsafeRepeat("x", 10)
go-extend 裡面還收錄了很多實用的方法,大家也可以多關注。
總結
- 千萬不要為了使用
[]byte
來優化string
傳遞,型別轉換成本很高,且slice
本身也比string
更大一些。 - 程式中是使用
string
還是[]byte
需要根據資料來源和處理資料的函式來決定,一定要減少型別轉換。 - 關於使用
strings
還是bytes
包的問題,主要關注點是資料原始型別以及想獲得的資料型別來選擇。 - 減少使用字串指標來優化字串,這會增加
GC
的開銷,具體可以參考 大堆中避免大量的 GC 開銷 一文。
轉載:
本文作者: 戚銀(thinkeridea)
本文連結: https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 CC BY 4.0 CN 協議 許可協議。轉載請註明出處!
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Java I/O 操作及優化建議Java優化
- Redis優化建議Redis優化
- EntityFramework 優化建議Framework優化
- Mysql優化建議MySql優化
- 十個常見的快取使用誤區及建議快取
- Zabbix 5.0 優化建議優化
- html前端優化建議HTML前端優化
- 頁面優化建議優化
- 資料庫優化建議資料庫優化
- mysql優化 | 儲存引擎,建表,索引,sql的優化建議MySql優化儲存引擎索引
- 【建議】 暫提三種優化優化
- MySQL InnoDB行鎖優化建議MySql優化
- 優化MySQL的21個建議優化MySql
- oracle 效能優化建議小結Oracle優化
- 優化 Webpack 構建效能的幾點建議優化Web
- 優化Webpack構建效能的幾點建議優化Web
- MySQL 調優/優化的 100 個建議MySql優化
- UITableView效能優化的幾點建議UIView優化
- 對SGA和PGA的優化建議優化
- 前端效能優化(三)——傳統 JavaScript 優化的誤區前端優化JavaScript
- 效能優化漫談之七:效能優化的誤區優化
- MySQL 高效能優化規範建議MySql優化
- 頁面優化的一些建議優化
- PHP程式碼優化的40條建議PHP優化
- 優化python程式的幾點建議優化Python
- Python效能優化的20條建議Python優化
- Android 優化APP 構建速度的17條建議Android優化APP
- Android 優化 APP 構建速度的 17 條建議Android優化APP
- 優化 Go 錯誤處理,魔改 Go recover 靠譜嗎?優化Go
- mysql鎖機制總結,以及優化建議MySql優化
- Nginx配置檔案詳解與優化建議Nginx優化
- (1) Mysql高效能優化規範建議MySql優化
- CSS 寫作建議和效能優化小結CSS優化
- ORACLE SQL 效能優化的一些建議OracleSQL優化
- 雅虎軍規——前端優化的35條建議前端優化
- .NET程式的效能要領和優化建議優化
- iOS當中4種UI元素的可用性問題及優化建議iOSUI優化
- Go 程式碼審查建議Go