簡體   English   中英

如何使用不安全從沒有 memory 復制的字符串中獲取字節切片

[英]How to use unsafe get a byte slice from a string without memory copy

我已閱讀有關從[]bytestring的無復制轉換的“ https://github.com/golang/go/issues/25484 ”。

我想知道是否有一種方法可以在沒有 memory 副本的情況下將字符串轉換為字節切片?

我正在編寫一個處理 terra 字節數據的程序,如果每個字符串在 memory 中復制兩次,則會減慢進度。 而且我不關心可變/不安全,只關心內部使用,我只需要盡可能快的速度。

例子:

var s string
// some processing on s, for some reasons, I must use string here
// ...
// then output to a writer
gzipWriter.Write([]byte(s))  // !!! Here I want to avoid the memory copy, no WriteString

所以問題是:有沒有辦法防止 memory 復制? 我知道也許我需要不安全的 package,但我不知道怎么做。 我已經搜索了一段時間,直到現在還沒有答案,所以 SO 都沒有顯示相關的答案。

通常只能使用unsafestring的內容作為[]byte獲取而不進行復制,因為 Go 中的string是不可變的,並且沒有副本就可以修改string的內容(通過更改字節切片)。

所以使用unsafe ,這就是它的樣子(更正的,有效的解決方案):

func unsafeGetBytes(s string) []byte {
    return (*[0x7fff0000]byte)(unsafe.Pointer(
        (*reflect.StringHeader)(unsafe.Pointer(&s)).Data),
    )[:len(s):len(s)]
}

此解決方案來自Ian Lance Taylor

原來,錯誤的解決方案是:

func unsafeGetBytesWRONG(s string) []byte {
    return *(*[]byte)(unsafe.Pointer(&s)) // WRONG!!!!
}

請參閱下面的Nuno Cruces 回答以進行推理。

測試它:

s := "hi"
data := unsafeGetBytes(s)
fmt.Println(data, string(data))

data = unsafeGetBytes("gopher")
fmt.Println(data, string(data))

輸出(在Go Playground上試試):

[104 105] hi
[103 111 112 104 101 114] gopher

但是:你寫你想要這個是因為你需要性能。 您還提到要壓縮數據。 請注意,壓縮數據(使用gzip )需要更多的計算,而不僅僅是復制幾個字節! 使用它你不會看到任何明顯的性能提升!

相反,當您想將string寫入io.Writer ,建議通過io.WriteString()函數執行此操作,如果可能,該函數將在不復制string (通過檢查並調用WriteString()方法,如果存在很可能比復制string更好)。 詳情請參見ResponseWriter.Write 和 io.WriteString 的區別是什么?

還有一些方法可以訪問string的內容而不將其轉換為[]byte ,例如索引,或使用編譯器優化副本的循環:

s := "something"
for i, v := range []byte(s) { // Copying s is optimized away
    // ...
}

另見相關問題:

golang: []byte(string) vs []byte(*string)

在 go 中使用從 []byte 到 string 的不安全轉換可能產生的后果是什么?

Go 中的字符串和 []byte 有什么區別?

Go 中別名類型之間的轉換是否會創建副本?

內部類型轉換是如何工作的? 相同的內存利用率是多少?

接受的答案現在有一個更好的、權威的、來自 Ian Lance Taylor 的解決方案。 我的在實踐中運行良好(AFAIK),但違反了unsafe.Pointer規則編號 1,這意味着它“今天可能無效或將來無效”。 所以使用伊恩的。

在 go 1.17 中,推薦使用unsafe.Slice


接受的答案是錯誤的,可能會產生評論中提到的恐慌@RFC。 @icza 關於 GC 和 keep alive 的解釋被誤導了。

容量為零(甚至是任意值)的原因更為平淡。

切片是:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

一個字符串是:

type StringHeader struct {
    Data uintptr
    Len  int
}

將字節切片轉換為字符串可以像strings.Builder那樣“安全地”完成:

func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

這會將Data指針和Len從切片復制到字符串。

相反的轉換並不“安全”,因為Cap沒有被設置為正確的值。

這是正確的代碼,可以修復恐慌:

var buf = *(*[]byte)(unsafe.Pointer(&str))
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(str)

也許:

var buf []byte
*(*string)(unsafe.Pointer(&buf)) = str
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(str)

我應該補充一點,所有這些轉換都是不安全的,因為字符串應該是不可變的,而字節數組/切片是可變的。

但是,如果您確定字節切片不會發生變異,則上述轉換不會出現邊界(或 GC)問題。

經過一些廣泛的調查,我相信我已經發現了從 Go 1.17 開始從string中獲取[]byte的最有效方法(這是針對 i386/x86_64 gc ;我還沒有測試其他架構。)權衡但是,這里的高效代碼對代碼來說是低效的。

在我說其他任何事情之前,應該明確的是,差異最終非常小並且可能無關緊要——以下信息僅用於娛樂/教育目的。


概括

通過一些小的改動,說明切片指向數組的技術的公認答案是最有效的方法。 話雖如此,如果unsafe.Slice在未來成為(決定性的)更好的選擇,我不會感到驚訝。


不安全的.Slice

unsafe.Slice目前的優勢是可讀性稍強,但我對它的性能持懷疑態度。 看起來它調用了runtime.unsafeslice 以下是Atamiri 的回答中提供的函數的 gc amd64 1.17 程序集(省略了FUNCDATA )。 注意堆棧檢查(缺少NOSPLIT ):

unsafeGetBytes_pc0:
        TEXT    "".unsafeGetBytes(SB), ABIInternal, $48-16
        CMPQ    SP, 16(R14)
        PCDATA  $0, $-2
        JLS     unsafeGetBytes_pc86
        PCDATA  $0, $-1
        SUBQ    $48, SP
        MOVQ    BP, 40(SP)
        LEAQ    40(SP), BP

        PCDATA  $0, $-2
        MOVQ    BX, ""..autotmp_4+24(SP)
        MOVQ    AX, "".s+56(SP)
        MOVQ    BX, "".s+64(SP)
        MOVQ    "".s+56(SP), DX
        PCDATA  $0, $-1
        MOVQ    DX, ""..autotmp_5+32(SP)
        LEAQ    type.uint8(SB), AX
        MOVQ    BX, CX
        MOVQ    DX, BX
        PCDATA  $1, $1
        CALL    runtime.unsafeslice(SB)
        MOVQ    ""..autotmp_5+32(SP), AX
        MOVQ    ""..autotmp_4+24(SP), BX
        MOVQ    BX, CX
        MOVQ    40(SP), BP
        ADDQ    $48, SP
        RET
unsafeGetBytes_pc86:
        NOP
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        MOVQ    AX, 8(SP)
        MOVQ    BX, 16(SP)
        CALL    runtime.morestack_noctxt(SB)
        MOVQ    8(SP), AX
        MOVQ    16(SP), BX
        PCDATA  $0, $-1
        JMP     unsafeGetBytes_pc0

關於上述其他不重要的有趣事實(很容易更改):編譯大小為3326 B; 內聯成本為7 正確的逃逸分析: s leaks to ~r1 with derefs=0


仔細修改 *reflect.SliceHeader

這種方法的優點/缺點是讓人們直接修改切片的內部狀態。 不幸的是,由於它的多行特性和 uintptr 的使用,如果不小心保留對原始字符串的引用,GC 很容易把事情搞砸。 (這里我避免創建臨時指針以減少內聯成本並避免需要添加runtime.KeepAlive ):

func unsafeGetBytes(s string) (b []byte) {
    (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
    (*reflect.SliceHeader)(unsafe.Pointer(&b)).Cap = len(s)
    (*reflect.SliceHeader)(unsafe.Pointer(&b)).Len = len(s)
    return
}

amd64 上的相應程序集(省略FUNCDATA ):

        TEXT    "".unsafeGetBytes(SB), NOSPLIT|ABIInternal, $32-16
        SUBQ    $32, SP
        MOVQ    BP, 24(SP)
        LEAQ    24(SP), BP

        MOVQ    AX, "".s+40(SP)
        MOVQ    BX, "".s+48(SP)
        MOVQ    $0, "".b(SP)
        MOVUPS  X15, "".b+8(SP)
        MOVQ    "".s+40(SP), DX
        MOVQ    DX, "".b(SP)
        MOVQ    "".s+48(SP), CX
        MOVQ    CX, "".b+16(SP)
        MOVQ    "".s+48(SP), BX
        MOVQ    BX, "".b+8(SP)
        MOVQ    "".b(SP), AX
        MOVQ    24(SP), BP
        ADDQ    $32, SP
        RET

關於上述其他不重要的有趣事實(很容易更改):編譯大小為3700 B; 內聯成本為20 低於標准的逃逸分析: s leaks to {heap} with derefs=0


修改 SliceHeader 的不安全版本

改編自Nuno Cruces 的回答 這依賴於StringHeaderSliceHeader之間固有的結構相似性,因此在某種意義上它“更容易”打破。 此外,它會暫時創建一個非法狀態,其中cap(b) (為0 )小於len(b)

func unsafeGetBytes(s string) (b []byte) {
    *(*string)(unsafe.Pointer(&b)) = s
    (*reflect.SliceHeader)(unsafe.Pointer(&b)).Cap = len(s)
    return
}

對應匯編(省略FUNCDATA ):

        TEXT    "".unsafeGetBytes(SB), NOSPLIT|ABIInternal, $32-16
        SUBQ    $32, SP
        MOVQ    BP, 24(SP)
        LEAQ    24(SP), BP
        MOVQ    AX, "".s+40(FP)

        MOVQ    $0, "".b(SP)
        MOVUPS  X15, "".b+8(SP)
        MOVQ    AX, "".b(SP)
        MOVQ    BX, "".b+8(SP)
        MOVQ    BX, "".b+16(SP)
        MOVQ    "".b(SP), AX
        MOVQ    BX, CX
        MOVQ    24(SP), BP
        ADDQ    $32, SP
        NOP
        RET

其他不重要的細節:編譯大小為3636 B,內聯成本為11 ,帶有低於標准的逃逸分析: s leaks to {heap} with derefs=0


切片指向數組的指針

這是公認的答案(此處顯示用於比較)——它的主要缺點是它的丑陋(即幻數0x7fff0000 )。 還有一個最小的可能性是得到比數組大的字符串,以及不可避免的邊界檢查。

func unsafeGetBytes(s string) []byte {
    return (*[0x7fff0000]byte)(unsafe.Pointer(
        (*reflect.StringHeader)(unsafe.Pointer(&s)).Data),
    )[:len(s):len(s)]
}

相應的程序集(刪除了FUNCDATA )。

        TEXT    "".unsafeGetBytes(SB), NOSPLIT|ABIInternal, $24-16
        SUBQ    $24, SP
        MOVQ    BP, 16(SP)
        LEAQ    16(SP), BP

        PCDATA  $0, $-2
        MOVQ    AX, "".s+32(SP)
        MOVQ    BX, "".s+40(SP)
        MOVQ    "".s+32(SP), AX
        PCDATA  $0, $-1
        TESTB   AL, (AX)
        NOP
        CMPQ    BX, $2147418112
        JHI     unsafeGetBytes_pc54
        MOVQ    BX, CX
        MOVQ    16(SP), BP
        ADDQ    $24, SP
        RET
unsafeGetBytes_pc54:
        MOVQ    BX, DX
        MOVL    $2147418112, BX
        PCDATA  $1, $1
        NOP
        CALL    runtime.panicSlice3Alen(SB)
        XCHGL   AX, AX

其他不重要的細節:編譯大小3142 B,內聯成本9 ,正確的逃逸分析: s leaks to ~r1 with derefs=0

注意runtime.panicSlice3Alen這是檢查len(s)是否在0x7fff0000內的邊界檢查。


改進了指向數組的切片指針

這是我認為自 Go 1.17 起最有效的方法。 我基本上修改了接受的答案以消除邊界檢查,並發現了一個“更有意義”的常量( math.MaxInt32 )使用比0x7fff0000 使用MaxInt32保留 32 位兼容性。

func unsafeGetBytes(s string) []byte {
    const MaxInt32 = 1<<31 - 1
    return (*[MaxInt32]byte)(unsafe.Pointer((*reflect.StringHeader)(
                    unsafe.Pointer(&s)).Data))[:len(s)&MaxInt32:len(s)&MaxInt32]
}

相應的程序集(刪除了FUNCDATA ):

        TEXT    "".unsafeGetBytes(SB), NOSPLIT|ABIInternal, $0-16

        PCDATA  $0, $-2
        MOVQ    AX, "".s+8(SP)
        MOVQ    BX, "".s+16(SP)
        MOVQ    "".s+8(SP), AX
        PCDATA  $0, $-1
        TESTB   AL, (AX)
        ANDQ    $2147483647, BX
        MOVQ    BX, CX
        RET

其他不重要的細節:編譯大小為3188 B,內聯成本為13 ,以及正確的逃逸分析: s leaks to ~r1 with derefs=0


我設法通過以下方式實現了目標:

func TestString(t *testing.T) {

    b := []byte{'a', 'b', 'c', '1', '2', '3', '4'}
    s := *(*string)(unsafe.Pointer(&b))
    sb := *(*[]byte)(unsafe.Pointer(&s))

    addr1 := unsafe.Pointer(&b)
    addr2 := unsafe.Pointer(&s)
    addr3 := unsafe.Pointer(&sb)

    fmt.Print("&b=", addr1, "\n&s=", addr2, "\n&sb=", addr3, "\n")

    hdr1 := (*reflect.StringHeader)(unsafe.Pointer(&b))
    hdr2 := (*reflect.SliceHeader)(unsafe.Pointer(&s))
    hdr3 := (*reflect.SliceHeader)(unsafe.Pointer(&sb))

    fmt.Print("b.data=", hdr1.Data, "\ns.data=", hdr2.Data, "\nsb.data=", hdr3.Data, "\n")

    b[0] = 'X'
    sb[1] = 'Y'  // if sb is from a string directly, this will cause nil panic
    fmt.Print("s=", s, "\nsb=")
    for _, c := range sb {
        fmt.Printf("%c", c)
    }
    fmt.Println()

}

輸出:

=== RUN   TestString
&b=0xc000218000
&s=0xc00021a000
&sb=0xc000218020
b.data=824635867152
s.data=824635867152
sb.data=824635867152
s=XYc1234
sb=XYc1234

這些變量都共享相同的內存。

在 Go 1.17 中,現在可以使用unsafe.Slice ,因此可以將接受的答案改寫如下:

func unsafeGetBytes(s string) []byte {
        return unsafe.Slice((*byte)(unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&s)).Data)), len(s))
}

簡單,沒有反射,而且我認為它是便攜的。 s是你的字符串, b是你的字節切片

var b []byte
bb:=(*[3]uintptr)(unsafe.Pointer(&b))[:]
copy(bb, (*[2]uintptr)(unsafe.Pointer(&s))[:])
bb[2] = bb[1]
// use b

請記住,不應修改字節值(會恐慌)。 重新切片是可以的(例如: bytes.split(b, []byte{','}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM