1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        你真的懂string與[]byte的轉(zhuǎn)換了嗎

        共 18924字,需瀏覽 38分鐘

         ·

        2021-09-23 21:34

        string類型和[]byte類型是我們編程時(shí)最常使用到的數(shù)據(jù)結(jié)構(gòu)。本文將探討兩者之間的轉(zhuǎn)換方式,通過(guò)分析它們之間的內(nèi)在聯(lián)系來(lái)?yè)荛_(kāi)迷霧。

        兩種轉(zhuǎn)換方式


        • 標(biāo)準(zhǔn)轉(zhuǎn)換

        go中string與[]byte的互換,相信每一位gopher都能立刻想到以下的轉(zhuǎn)換方式,我們將之稱為標(biāo)準(zhǔn)轉(zhuǎn)換。


        1// string to []byte
        2s1 := "hello"
        3b := []byte(s1)
        4
        5// []byte to string
        6s2 := string(b)


        • 強(qiáng)轉(zhuǎn)換

        通過(guò)unsafe和reflect包,可以實(shí)現(xiàn)另外一種轉(zhuǎn)換方式,我們將之稱為強(qiáng)轉(zhuǎn)換(也常常被人稱作黑魔法)。


         1func String2Bytes(s string) []byte {
        2    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
        3    bh := reflect.SliceHeader{
        4        Data: sh.Data,
        5        Len:  sh.Len,
        6        Cap:  sh.Len,
        7    }
        8    return *(*[]byte)(unsafe.Pointer(&bh))
        9}
        10
        11func Bytes2String(b []byte) string {
        12    return *(*string)(unsafe.Pointer(&b))
        13}


        • 性能對(duì)比

        既然有兩種轉(zhuǎn)換方式,那么我們有必要對(duì)它們做性能對(duì)比。


         1// 測(cè)試強(qiáng)轉(zhuǎn)換功能
        2func TestBytes2String(t *testing.T) {
        3    x := []byte("Hello Gopher!")
        4    y := Bytes2String(x)
        5    z := string(x)
        6
        7    if y != z {
        8        t.Fail()
        9    }
        10}
        11
        12// 測(cè)試強(qiáng)轉(zhuǎn)換功能
        13func TestString2Bytes(t *testing.T) {
        14    x := "Hello Gopher!"
        15    y := String2Bytes(x)
        16    z := []byte(x)
        17
        18    if !bytes.Equal(y, z) {
        19        t.Fail()
        20    }
        21}
        22
        23// 測(cè)試標(biāo)準(zhǔn)轉(zhuǎn)換string()性能
        24func Benchmark_NormalBytes2String(b *testing.B) {
        25    x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
        26    for i := 0; i < b.N; i++ {
        27        _ = string(x)
        28    }
        29}
        30
        31// 測(cè)試強(qiáng)轉(zhuǎn)換[]byte到string性能
        32func Benchmark_Byte2String(b *testing.B) {
        33    x := []byte("Hello Gopher! Hello Gopher! Hello Gopher!")
        34    for i := 0; i < b.N; i++ {
        35        _ = Bytes2String(x)
        36    }
        37}
        38
        39// 測(cè)試標(biāo)準(zhǔn)轉(zhuǎn)換[]byte性能
        40func Benchmark_NormalString2Bytes(b *testing.B) {
        41    x := "Hello Gopher! Hello Gopher! Hello Gopher!"
        42    for i := 0; i < b.N; i++ {
        43        _ = []byte(x)
        44    }
        45}
        46
        47// 測(cè)試強(qiáng)轉(zhuǎn)換string到[]byte性能
        48func Benchmark_String2Bytes(b *testing.B) {
        49    x := "Hello Gopher! Hello Gopher! Hello Gopher!"
        50    for i := 0; i < b.N; i++ {
        51        _ = String2Bytes(x)
        52    }
        53}


        測(cè)試結(jié)果如下


         1go test -bench="." -benchmem
        2goos: darwin
        3goarch: amd64
        4pkg: workspace/example/stringBytes
        5Benchmark_NormalBytes2String-8          38363413                27.9 ns/op            48 B/op          1 allocs/op
        6Benchmark_Byte2String-8                 1000000000               0.265 ns/op           0 B/op          0 allocs/op
        7Benchmark_NormalString2Bytes-8          32577080                34.8 ns/op            48 B/op          1 allocs/op
        8Benchmark_String2Bytes-8                1000000000               0.532 ns/op           0 B/op          0 allocs/op
        9PASS
        10ok      workspace/example/stringBytes   3.170s


        注意,-benchmem可以提供每次操作分配內(nèi)存的次數(shù),以及每次操作分配的字節(jié)數(shù)。

        當(dāng)x的數(shù)據(jù)均為"Hello Gopher!"時(shí),測(cè)試結(jié)果如下


         1go test -bench="." -benchmem
        2goos: darwin
        3goarch: amd64
        4pkg: workspace/example/stringBytes
        5Benchmark_NormalBytes2String-8          245907674                4.86 ns/op            0 B/op          0 allocs/op
        6Benchmark_Byte2String-8                 1000000000               0.266 ns/op           0 B/op          0 allocs/op
        7Benchmark_NormalString2Bytes-8          202329386                5.92 ns/op            0 B/op          0 allocs/op
        8Benchmark_String2Bytes-8                1000000000               0.532 ns/op           0 B/op          0 allocs/op
        9PASS
        10ok      workspace/example/stringBytes   4.383s


        強(qiáng)轉(zhuǎn)換方式的性能會(huì)明顯優(yōu)于標(biāo)準(zhǔn)轉(zhuǎn)換。


        讀者可以思考以下問(wèn)題

        1.為什么強(qiáng)轉(zhuǎn)換性能會(huì)比標(biāo)準(zhǔn)轉(zhuǎn)換好?

        2.為什么在上述測(cè)試中,當(dāng)x的數(shù)據(jù)較大時(shí),標(biāo)準(zhǔn)轉(zhuǎn)換方式會(huì)有一次分配內(nèi)存的操作,從而導(dǎo)致其性能更差,而強(qiáng)轉(zhuǎn)換方式卻不受影響?

        3.既然強(qiáng)轉(zhuǎn)換方式性能這么好,為什么go語(yǔ)言提供給我們使用的是標(biāo)準(zhǔn)轉(zhuǎn)換方式?


        原理分析


        要回答以上三個(gè)問(wèn)題,首先要明白是string和[]byte在go中到底是什么。

        • []byte

        在go中,byte是uint8的別名,在go標(biāo)準(zhǔn)庫(kù)builtin中有如下說(shuō)明:

        1// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
        2// used, by convention, to distinguish byte values from 8-bit unsigned
        3// integer values.
        4type byte = uint8


        在go的源碼中src/runtime/slice.go,slice的定義如下:


        1type slice struct {
        2    array unsafe.Pointer
        3    len   int
        4    cap   int
        5}


        array是底層數(shù)組的指針,len表示長(zhǎng)度,cap表示容量。對(duì)于[]byte來(lái)說(shuō),array指向的就是byte數(shù)組。


        • string

        關(guān)于string類型,在go標(biāo)準(zhǔn)庫(kù)builtin中有如下說(shuō)明:


        1// string is the set of all strings of 8-bit bytes, conventionally but not
        2// necessarily representing UTF-8-encoded text. A string may be empty, but
        3// not nil. Values of string type are immutable.
        4type string string


        翻譯過(guò)來(lái)就是:string是8位字節(jié)的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但是不能為nil。string的值是不能改變的。

        在go的源碼中src/runtime/string.go,string的定義如下:


        1type stringStruct struct {
        2    str unsafe.Pointer
        3    len int
        4}


        stringStruct代表的就是一個(gè)string對(duì)象,str指針指向的是某個(gè)數(shù)組的首地址,len代表的數(shù)組長(zhǎng)度。那么這個(gè)數(shù)組是什么呢?我們可以在實(shí)例化stringStruct對(duì)象時(shí)找到答案。


        1//go:nosplit
        2func gostringnocopy(str *byte) string {
        3    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
        4    s := *(*string)(unsafe.Pointer(&ss))
        5    return s
        6}


        可以看到,入?yún)tr指針就是指向byte的指針,那么我們可以確定string的底層數(shù)據(jù)結(jié)構(gòu)就是byte數(shù)組。



        綜上,string與[]byte在底層結(jié)構(gòu)上是非常的相近(后者的底層表達(dá)僅多了一個(gè)cap屬性,因此它們?cè)趦?nèi)存布局上是可對(duì)齊的),這也就是為何builtin中內(nèi)置函數(shù)copy會(huì)有一種特殊情況copy(dst []byte, src string) int的原因了。


        1// The copy built-in function copies elements from a source slice into a
        2// destination slice. (As a special case, it also will copy bytes from a
        3// string to a slice of bytes.) The source and destination may overlap. Copy
        4// returns the number of elements copied, which will be the minimum of
        5// len(src) and len(dst).
        6func copy(dst, src []Type) int
        7
        • 區(qū)別

        對(duì)于[]byte與string而言,兩者之間最大的區(qū)別就是string的值不能改變。這該如何理解呢?下面通過(guò)兩個(gè)例子來(lái)說(shuō)明。

        對(duì)于[]byte來(lái)說(shuō),以下操作是可行的:


        1b := []byte("Hello Gopher!")
        2b [1] = 'T'


        string,修改操作是被禁止的


        1s := "Hello Gopher!"
        2s[1] = 'T'


        而string能支持這樣的操作:


        1s := "Hello Gopher!"
        2s = "Tello Gopher!"


        字符串的值不能被更改,但可以被替換。string在底層都是結(jié)構(gòu)體stringStruct{str: str_point, len: str_len},string結(jié)構(gòu)體的str指針指向的是一個(gè)字符常量的地址, 這個(gè)地址里面的內(nèi)容是不可以被改變的,因?yàn)樗侵蛔x的,但是這個(gè)指針可以指向不同的地址。


        那么,以下操作的含義是不同的:


        1s := "S1" // 分配存儲(chǔ)"S1"的內(nèi)存空間,s結(jié)構(gòu)體里的str指針指向這塊內(nèi)存
        2s = "S2"  // 分配存儲(chǔ)"S2"的內(nèi)存空間,s結(jié)構(gòu)體里的str指針轉(zhuǎn)為指向這塊內(nèi)存
        3
        4b := []byte{1// 分配存儲(chǔ)'1'數(shù)組的內(nèi)存空間,b結(jié)構(gòu)體的array指針指向這個(gè)數(shù)組。
        5b = []byte{2}  // 將array的內(nèi)容改為'2'


        圖解如下



        因?yàn)閟tring的指針指向的內(nèi)容是不可以更改的,所以每更改一次字符串,就得重新分配一次內(nèi)存,之前分配的空間還需要gc回收,這是導(dǎo)致string相較于[]byte操作低效的根本原因。

        • 標(biāo)準(zhǔn)轉(zhuǎn)換的實(shí)現(xiàn)細(xì)節(jié)

        []byte(string)的實(shí)現(xiàn)(源碼在src/runtime/string.go中)



         1// The constant is known to the compiler.
        2// There is no fundamental theory behind this number.
        3const tmpStringBufSize = 32
        4
        5type tmpBuf [tmpStringBufSize]byte
        6
        7func stringtoslicebyte(buf *tmpBuf, s string) []byte {
        8    var b []byte
        9    if buf != nil && len(s) <= len(buf) {
        10        *buf = tmpBuf{}
        11        b = buf[:len(s)]
        12    } else {
        13        b = rawbyteslice(len(s))
        14    }
        15    copy(b, s)
        16    return b
        17}
        18
        19// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
        20func rawbyteslice(size int) (b []byte) {
        21    cap := roundupsize(uintptr(size))
        22    p := mallocgc(capnilfalse)
        23    if cap != uintptr(size) {
        24        memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
        25    }
        26
        27    *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
        28    return
        29}


        這里有兩種情況:s的長(zhǎng)度是否大于32。當(dāng)大于32時(shí),go需要調(diào)用mallocgc分配一塊新的內(nèi)存(大小由s決定),這也就回答了上文中的問(wèn)題2:當(dāng)x的數(shù)據(jù)較大時(shí),標(biāo)準(zhǔn)轉(zhuǎn)換方式會(huì)有一次分配內(nèi)存的操作。

        最后通過(guò)copy函數(shù)實(shí)現(xiàn)string到[]byte的拷貝,具體實(shí)現(xiàn)在src/runtime/slice.go中的slicestringcopy方法。


         1func slicestringcopy(to []byte, fm string) int {
        2    if len(fm) == 0 || len(to) == 0 {
        3        return 0
        4    }
        5
        6  // copy的長(zhǎng)度取決與string和[]byte的長(zhǎng)度最小值
        7    n := len(fm)
        8    if len(to) < n {
        9        n = len(to)
        10    }
        11
        12  // 如果開(kāi)啟了競(jìng)態(tài)檢測(cè) -race
        13    if raceenabled {
        14        callerpc := getcallerpc()
        15        pc := funcPC(slicestringcopy)
        16        racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
        17    }
        18  // 如果開(kāi)啟了memory sanitizer -msan
        19    if msanenabled {
        20        msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
        21    }
        22
        23  // 該方法將string的底層數(shù)組從頭部復(fù)制n個(gè)到[]byte對(duì)應(yīng)的底層數(shù)組中去(這里就是copy實(shí)現(xiàn)的核心方法,在匯編層面實(shí)現(xiàn) 源文件為memmove_*.s)
        24    memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
        25    return n
        26}


        copy實(shí)現(xiàn)過(guò)程圖解如下



        string([]byte)的實(shí)現(xiàn)(源碼也在src/runtime/string.go中)


         1// Buf is a fixed-size buffer for the result,
        2// it is not nil if the result does not escape.
        3func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
        4    l := len(b)
        5    if l == 0 {
        6        // Turns out to be a relatively common case.
        7        // Consider that you want to parse out data between parens in "foo()bar",
        8        // you find the indices and convert the subslice to string.
        9        return ""
        10    }
        11  // 如果開(kāi)啟了競(jìng)態(tài)檢測(cè) -race
        12    if raceenabled {
        13        racereadrangepc(unsafe.Pointer(&b[0]),
        14            uintptr(l),
        15            getcallerpc(),
        16            funcPC(slicebytetostring))
        17    }
        18  // 如果開(kāi)啟了memory sanitizer -msan
        19    if msanenabled {
        20        msanread(unsafe.Pointer(&b[0]), uintptr(l))
        21    }
        22    if l == 1 {
        23        stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
        24        stringStructOf(&str).len = 1
        25        return
        26    }
        27
        28    var p unsafe.Pointer
        29    if buf != nil && len(b) <= len(buf) {
        30        p = unsafe.Pointer(buf)
        31    } else {
        32        p = mallocgc(uintptr(len(b)), nilfalse)
        33    }
        34    stringStructOf(&str).str = p
        35    stringStructOf(&str).len = len(b)
        36  // 拷貝字節(jié)數(shù)組至字符串
        37    memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
        38    return
        39}
        40
        41// 實(shí)例stringStruct對(duì)象
        42func stringStructOf(sp *string) *stringStruct {
        43    return (*stringStruct)(unsafe.Pointer(sp))
        44}


        可見(jiàn),當(dāng)數(shù)組長(zhǎng)度超過(guò)32時(shí),同樣需要調(diào)用mallocgc分配一塊新內(nèi)存。最后通過(guò)memmove完成拷貝。

        • 強(qiáng)轉(zhuǎn)換的實(shí)現(xiàn)細(xì)節(jié)

        1. 萬(wàn)能的unsafe.Pointer指針

        在go中,任何類型的指針*T都可以轉(zhuǎn)換為unsafe.Pointer類型的指針,它可以存儲(chǔ)任何變量的地址。同時(shí),unsafe.Pointer類型的指針也可以轉(zhuǎn)換回普通指針,而且可以不必和之前的類型*T相同。另外,unsafe.Pointer類型還可以轉(zhuǎn)換為uintptr類型,該類型保存了指針?biāo)赶虻刂返臄?shù)值,從而可以使我們對(duì)地址進(jìn)行數(shù)值計(jì)算。以上就是強(qiáng)轉(zhuǎn)換方式的實(shí)現(xiàn)依據(jù)。

        而string和slice在reflect包中,對(duì)應(yīng)的結(jié)構(gòu)體是reflect.StringHeader和reflect.SliceHeader,它們是string和slice的運(yùn)行時(shí)表達(dá)。

         1type StringHeader struct {
        2    Data uintptr
        3    Len  int
        4}
        5
        6type SliceHeader struct {
        7    Data uintptr
        8    Len  int
        9    Cap  int
        10}

        2. 內(nèi)存布局

        從string和slice的運(yùn)行時(shí)表達(dá)可以看出,除了SilceHeader多了一個(gè)int類型的Cap字段,Date和Len字段是一致的。所以,它們的內(nèi)存布局是可對(duì)齊的,這說(shuō)明我們就可以直接通過(guò)unsafe.Pointer進(jìn)行轉(zhuǎn)換。

        []byte轉(zhuǎn)string圖解

        string轉(zhuǎn)[]byte圖解


        • Q&A

        Q1.為什么強(qiáng)轉(zhuǎn)換性能會(huì)比標(biāo)準(zhǔn)轉(zhuǎn)換好?

        對(duì)于標(biāo)準(zhǔn)轉(zhuǎn)換,無(wú)論是從[]byte轉(zhuǎn)string還是string轉(zhuǎn)[]byte都會(huì)涉及底層數(shù)組的拷貝。而強(qiáng)轉(zhuǎn)換是直接替換指針的指向,從而使得string和[]byte指向同一個(gè)底層數(shù)組。這樣,當(dāng)然后者的性能會(huì)更好。


        Q2.為什么在上述測(cè)試中,當(dāng)x的數(shù)據(jù)較大時(shí),標(biāo)準(zhǔn)轉(zhuǎn)換方式會(huì)有一次分配內(nèi)存的操作,從而導(dǎo)致其性能更差,而強(qiáng)轉(zhuǎn)換方式卻不受影響?

        標(biāo)準(zhǔn)轉(zhuǎn)換時(shí),當(dāng)數(shù)據(jù)長(zhǎng)度大于32個(gè)字節(jié)時(shí),需要通過(guò)mallocgc申請(qǐng)新的內(nèi)存,之后再進(jìn)行數(shù)據(jù)拷貝工作。而強(qiáng)轉(zhuǎn)換只是更改指針指向。所以,當(dāng)轉(zhuǎn)換數(shù)據(jù)較大時(shí),兩者性能差距會(huì)愈加明顯。


        Q3.既然強(qiáng)轉(zhuǎn)換方式性能這么好,為什么go語(yǔ)言提供給我們使用的是標(biāo)準(zhǔn)轉(zhuǎn)換方式?

        首先,我們需要知道Go是一門類型安全的語(yǔ)言,而安全的代價(jià)就是性能的妥協(xié)。但是,性能的對(duì)比是相對(duì)的,這點(diǎn)性能的妥協(xié)對(duì)于現(xiàn)在的機(jī)器而言微乎其微。另外強(qiáng)轉(zhuǎn)換的方式,會(huì)給我們的程序帶來(lái)極大的安全隱患。

        如下示例

        1a := "hello"
        2b := String2Bytes(a)
        3b[0] = 'H'


        a是string類型,前面我們講到它的值是不可修改的。通過(guò)強(qiáng)轉(zhuǎn)換將a的底層數(shù)組賦給b,而b是一個(gè)[]byte類型,它的值是可以修改的,所以這時(shí)對(duì)底層數(shù)組的值進(jìn)行修改,將會(huì)造成嚴(yán)重的錯(cuò)誤(通過(guò)defer+recover也不能捕獲)。


        1unexpected fault address 0x10b6139
        2fatal error: fault
        3[signal SIGBUS: bus error code=0x2 addr=0x10b6139 pc=0x1088f2c]


        Q4. 為什么string要設(shè)計(jì)為不可修改?

        我認(rèn)為有必要思考一下該問(wèn)題。string不可修改,意味它是只讀屬性,這樣的好處就是:在并發(fā)場(chǎng)景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔(dān)心安全問(wèn)題。


        • 取舍場(chǎng)景

        1. 在你不確定安全隱患的條件下,盡量采用標(biāo)準(zhǔn)方式進(jìn)行數(shù)據(jù)轉(zhuǎn)換。

        2. 當(dāng)程序?qū)\(yùn)行性能有高要求,同時(shí)滿足對(duì)數(shù)據(jù)僅僅只有讀操作的條件,且存在頻繁轉(zhuǎn)換(例如消息轉(zhuǎn)發(fā)場(chǎng)景),可以使用強(qiáng)轉(zhuǎn)換。

           


        喜歡明哥文章的同學(xué)
        歡迎長(zhǎng)按下圖訂閱!

        ???

        瀏覽 56
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            色五月婷婷综合 | 攵女乱h系列合集多女国产剧 | 免费观看全黄做爰视频网站 | 夜夜夜操| 亚洲国产精品久久久久 | av簧片 | 欧美在线视频免费观看 | 美臀诱惑日韩在线一区二区三区 | 99在线国内精品自产拍 | 一部一直喷奶水的av |