[英]How to use unsafe get a byte slice from a string without memory copy
我已閱讀有關從[]byte
到string
的無復制轉換的“ 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 都沒有顯示相關的答案。
通常只能使用unsafe
將string
的內容作為[]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)
接受的答案現在有一個更好的、權威的、來自 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
在未來成為(決定性的)更好的選擇,我不會感到驚訝。
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
。
這種方法的優點/缺點是讓人們直接修改切片的內部狀態。 不幸的是,由於它的多行特性和 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
。
改編自Nuno Cruces 的回答。 這依賴於StringHeader
和SliceHeader
之間固有的結構相似性,因此在某種意義上它“更容易”打破。 此外,它會暫時創建一個非法狀態,其中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.