【Go】string 優(yōu)化誤區(qū)及建議

原文鏈接: https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html

本文原標題為 《string 也是引用類型》,經(jīng)過 郝林 大佬指點原標題存在誘導性沽一,這里解釋一下 "引用類型" 有兩個特征:1、多個變量引用一塊內(nèi)存數(shù)據(jù)漓糙,不創(chuàng)建變量的副本铣缠,2、修改任意變量的數(shù)據(jù),其它變量可見蝗蛙。顯然字符串只滿足了 "引用類型" 的第一個特點蝇庭,不能滿足第二個特點,顧不能說字符串是引用類型捡硅,感謝大佬指正哮内。

初學 Go 語言的朋友總會在傳 []bytestring 之間有著很多糾結(jié),實際上是沒有了解 stringslice 的本質(zhì)壮韭,而且讀了一些程序源碼北发,也發(fā)現(xiàn)很多與之相關(guān)的問題,下面類似的代碼估計很多初學者都寫過喷屋,也充分說明了作者當時內(nèi)心的糾結(jié):

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))
}

雖然這樣的代碼并不是來自真實的項目琳拨,但是確實有人這樣設(shè)計,單從設(shè)計上看就很糟糕了屯曹,這樣設(shè)計的原因很多人說:“slice 是引用類型狱庇,傳遞引用類型效率高呀”,主要原因不了解兩者的本質(zhì)恶耽。

上面這個例子如果覺得有點基礎(chǔ)和可愛密任,下面這個例子貌似并不那么容易說明其存在的問題了吧。

package main

func xx(s *string) *string{
    ....
    return s
}

func main(){
    s := "xx"
    
    s = *xx(&s)
    
    ss :=[]*string{}
    
    ss = append(ss, &s)
}

指針效率高偷俭,我就用指針多好浪讳,可以減少內(nèi)存分配呀,設(shè)計函數(shù)都接收指針變量社搅,程序性能會有很大提升驻债,在實際的項目中這種例子也不少見,我想通過這篇文檔來幫助初學者走出誤區(qū)形葬,減少適得其反的優(yōu)化技巧合呐。

slice 的定義

在之前 “【Go】深入剖析slice和array” 一文中說了 slice 在內(nèi)存中的存儲模式,slice 本身包含一個指向底層數(shù)組的指針笙以,一個 int 類型的長度和一個 int 類型的容量淌实, 這就是 slice 的本質(zhì), []byte 本身也是一個 slice猖腕,只是底層數(shù)組存儲的元素是 byte拆祈。下面這個圖就是 slice 的在內(nèi)存中的狀態(tài):

slice_1.jpg

看一下 reflect.SliceHeader 如何定義 slice 在內(nèi)存中的結(jié)構(gòu)吧:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

slice 是引用類型是 slice 本身會包含一個地址,在傳遞 slice 時只需要分配 SliceHeader 就好了倘感, 而 SliceHeader 只包含了三個 int 類型放坏,相當于傳遞一個 slice 就只需要拷貝 SliceHeader,而不用拷貝整個底層數(shù)組老玛,所以才說 slice 是引用類型的淤年。

那么字符串呢钧敞,計算機中我們處理的大多數(shù)問題都和字符串有關(guān),難道傳遞字符串真的需要那么高的成本麸粮,需要借助 slice 和指針來減少內(nèi)存開銷嗎溉苛。

string 的定義

reflect 包里面也定義了一個 StringHeader 看一下吧:

type StringHeader struct {
    Data uintptr
    Len  int
}

字符串只包含了兩個 int 類型的數(shù)據(jù),其中一個是指針弄诲,一個是字符串的長度愚战,從 StringHeader 定義來看 string 并不會發(fā)生拷貝的,傳遞 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
}

可以發(fā)現(xiàn)前三個輸出的指針都是同一個地址寂玲,第四個的地址發(fā)生了一個字節(jié)的偏移,分析來看傳遞字符串確實沒有分配新的內(nèi)存洛搀,同時和 slice 一樣即使傳遞字符串的子串也不會分配新的內(nèi)存空間敢茁,而是指向原字符串的中的一個位置。

這樣說來把 string 轉(zhuǎn)成 []byte 還浪費的一個 int 的空間呢留美,需要分配更多的內(nèi)存彰檬,真是適得其反呀,而且類型轉(zhuǎn)換會發(fā)生內(nèi)存拷貝谎砾,從 string 轉(zhuǎn)為 []byte 才是真的把 string 底層數(shù)據(jù)全部拷貝一遍呢逢倍,真是得不償失呀。

string 的兩個小特性

字符串還有兩個小特性景图,針對字面量(就是直接寫在程序中的字符串)较雕,會創(chuàng)建在只讀空間上,并且被復用挚币,看一下下面的一個小例子:

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)))
}

從輸出可以了解到亮蒋,相同的字面量會被復用,但是子串是不會復用空間的妆毕,這就是編譯器給我們帶來的福利了慎玖,可以減少字面量字符串占用的內(nèi)存空間。

(reflect.StringHeader) {
 Data: (uintptr) 0x10f5ea0,
 Len: (int) 2
}
(reflect.StringHeader) {
 Data: (uintptr) 0x10f5ea0,
 Len: (int) 2
}
(reflect.StringHeader) {
 Data: (uintptr) 0x10f5f2e,
 Len: (int) 3
}

另一個小特性大家都知道笛粘,就是字符串是不能修改的趁怔,如果我們不希望調(diào)用函數(shù)修改我們的數(shù)據(jù),最好傳遞字符串薪前,高效有安全润努。

不過有了 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)
}

從輸出里面居然發(fā)現(xiàn)字符串被修改了, 我們沒有辦法直接修改字符串铺浇,但是可以利用 slicestring 本身結(jié)構(gòu)的特性,創(chuàng)建一個 slice 讓它的指針指向 string 的指針位置垛膝,然后借助 unsafe 把這個 SliceHeader 轉(zhuǎn)成 []byte 來修改字符串随抠,字符串確實被修改了裁着。

xxxxxxxxxx
xaxxxxxxxx

看了上面的例子是不是開始擔心把字符串傳給其它函數(shù)真的不會更改嗎?感覺很不放心的樣子拱她,難道使用任何函數(shù)都要了解它的內(nèi)部實現(xiàn)嗎,其實這種情況極少發(fā)生扔罪,還記得之前說的那個字符串特性嗎秉沼,字面量字符串會放到只讀空間中,這個很重要矿酵,可以保證不是任何函數(shù)想修改我們的字符串就可以修改的唬复。

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'
}

運行上面的代碼發(fā)生了一個運行時不可修復的錯誤,就是這個特性其它函數(shù)不能確保輸入字符串是否是字面量全肮,也是不會惡意修改我們字符串的了敞咧。

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

關(guān)于字符串轉(zhuǎn) []bytego-extend 擴展包中有直接的實現(xiàn),這種用法在 go-extend 內(nèi)部方法實現(xiàn)中也有大量使用辜腺, 實際上因為原數(shù)據(jù)類型和處理數(shù)據(jù)的函數(shù)類型不一致休建,使用這種方法轉(zhuǎn)換字符串和 []byte 可以極大的提升程序性能

上面這兩個函數(shù)用的好测砂,可以極大的提升我們程序的性能,關(guān)于 exstrings.UnsafeToBytes 我們轉(zhuǎn)換不確定是否是字面量的字符串時就需要確保調(diào)用的函數(shù)不會修改我們的數(shù)據(jù)百匆,這往常在調(diào)用 bytes 里面的方法十分有效砌些。

傳字符串和字符串指針的區(qū)別

之前分析了傳遞 slice 并沒有 string 高效,何況轉(zhuǎn)換數(shù)據(jù)類型本身就會發(fā)生數(shù)據(jù)拷貝加匈。

那么在這篇文章的第二個例子存璃,為什么說傳遞字符串指針也不好呢,要了解指針在底層就是一個 int 類型的數(shù)據(jù)雕拼,而我們字符串只是兩個 int 而已纵东,另外如果了解 GC 的話,GC 只處理堆上的數(shù)據(jù)悲没,傳遞指針字符串會導致數(shù)據(jù)逃逸到堆上篮迎,閱讀標準庫的代碼會有很多注釋說明避免逃逸到堆上,這樣會極大的增加 GC 的開銷示姿,GC 的成本可謂是很高的呀甜橱。

疑惑

這篇文章說 “傳遞 slice 并沒有 string 高效”,為什么還會有 bytes 包的存在呢栈戳,其中很多函數(shù)的功能和 strings 包的功能一致岂傲,只是把 string 換成了 []byte, 既然傳遞 []byte 沒有 string 效率好子檀,這個包存在的意義是什么呢镊掖。

我們想一下轉(zhuǎn)換數(shù)據(jù)類型是會發(fā)生數(shù)據(jù)拷貝乃戈,這個成本可是大的多呀,如果我們數(shù)據(jù)本身就是 []byte 類型亩进,使用 strings 包就需要轉(zhuǎn)換數(shù)據(jù)類型了症虑。

另外我們對比兩個函數(shù)來看下一下即使傳遞 []byte 沒有 string 效率好,但是標準庫實現(xiàn)上卻會導致兩個函數(shù)有很大的性能差異的归薛。

strings.Repeat 函數(shù):

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 函數(shù):

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
}

上面兩個函數(shù)的實現(xiàn)非常相似谍憔,除了類型不同 strings 包在處理完數(shù)據(jù)發(fā)生了一次類型轉(zhuǎn)換,使用 bytes 只有一次內(nèi)存分配主籍,而 strings 是兩次习贫。

我們可以借助 exbytes.ToString 函數(shù)把 bytes.Repeat 的返回沒有任何成本的轉(zhuǎn)換會我們需要的字符串千元,如果我們輸入也是一個字符串的話,還可以借助 exstrings.UnsafeToBytes 來轉(zhuǎn)換輸入的數(shù)據(jù)類型幸海。

例如:

s := exbytes.ToString(bytes.Repeat(exstrings.UnsafeToBytes("x"), 10))

不過這樣寫有點太麻煩了,實際上 exstrings 包里面正在修改 strings 里面一些類似函數(shù)的問題涕烧,所有的實現(xiàn)基本和標準庫一致,只是把其中類型轉(zhuǎn)換的部分用 exbytes.ToString 優(yōu)化了一下议纯,可以提升性能父款,也能提升開發(fā)效率。

exstrings.UnsafeRepeat 函數(shù):

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)
}

如果用上面的函數(shù)只需要下面這樣寫就可以了:

s:=exstrings.UnsafeRepeat("x", 10)

go-extend 里面還收錄了很多實用的方法瞻凤,大家也可以多關(guān)注憨攒。

總結(jié)

  • 千萬不要為了使用 []byte 來優(yōu)化 string 傳遞,類型轉(zhuǎn)換成本很高阀参,且 slice 本身也比 string 更大一些肝集。
  • 程序中是使用 string 還是 []byte 需要根據(jù)數(shù)據(jù)來源和處理數(shù)據(jù)的函數(shù)來決定,一定要減少類型轉(zhuǎn)換蛛壳。
  • 關(guān)于使用 strings 還是 bytes 包的問題杏瞻,主要關(guān)注點是數(shù)據(jù)原始類型以及想獲得的數(shù)據(jù)類型來選擇。
  • 減少使用字符串指針來優(yōu)化字符串衙荐,這會增加 GC 的開銷捞挥,具體可以參考 大堆中避免大量的GC開銷 一文。

轉(zhuǎn)載:

本文作者: 戚銀(thinkeridea

本文鏈接: https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html

版權(quán)聲明: 本博客所有文章除特別聲明外忧吟,均采用 CC BY 4.0 CN協(xié)議 許可協(xié)議砌函。轉(zhuǎn)載請注明出處!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市讹俊,隨后出現(xiàn)的幾起案子垦沉,更是在濱河造成了極大的恐慌,老刑警劉巖仍劈,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厕倍,死亡現(xiàn)場離奇詭異,居然都是意外死亡耳奕,警方通過查閱死者的電腦和手機绑青,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋群,“玉大人,你說我怎么就攤上這事坏挠∩瞩铮” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵降狠,是天一觀的道長对竣。 經(jīng)常有香客問我,道長榜配,這世上最難降的妖魔是什么否纬? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮临燃,結(jié)果婚禮上膜廊,老公的妹妹穿的比我還像新娘淫茵。我一直安慰自己,他們只是感情好铆铆,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布薄货。 她就那樣靜靜地躺著菲驴,像睡著了一般骑冗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巧涧,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天谤绳,我揣著相機與錄音,去河邊找鬼消略。 笑死艺演,一個胖子當著我的面吹牛桐臊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伤提,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼肿男,長吁一口氣:“原來是場噩夢啊……” “哼次伶!你這毒婦竟也來了冠王?” 一聲冷哼從身側(cè)響起舌镶,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎餐胀,沒想到半個月后否灾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡挎狸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年锨匆,在試婚紗的時候發(fā)現(xiàn)自己被綠了恐锣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土榴。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡鞭衩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑞佩,到底是詐尸還是另有隱情,我是刑警寧澤瘫寝,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布焕阿,位于F島的核電站首启,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏褒纲。R本人自食惡果不足惜钥飞,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一彻秆、第九天 我趴在偏房一處隱蔽的房頂上張望唇兑。 院中可真熱鬧,春花似錦耻讽、人聲如沸帕棉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽低斋。三九已至,卻和暖如春掘猿,著一層夾襖步出監(jiān)牢的瞬間稠通,已是汗流浹背改橘。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工飞主, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奸远,地道東北人懒叛。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓薛窥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親佩番。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容