如何使用 unsafe 从没有内存复制的字符串中获取字节切片

sha*_*awn 4 string performance go slice

我已经阅读了关于“ https://github.com/golang/go/issues/25484 ”关于从[]byteto 的无复制转换string

我想知道是否有办法将字符串转换为没有内存复制的字节片?

我正在编写一个处理 terra 字节数据的程序,如果每个字符串在内存中复制两次,则会减慢进度。而且我不关心可变/不安全,只关心内部使用,我只需要尽可能快的速度。

例子:

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
Run Code Online (Sandbox Code Playgroud)

所以问题是:有没有办法防止内存复制?我知道也许我需要 unsafe 包,但我不知道如何。我已经搜索了一段时间,直到现在还没有答案,SO 也没有显示相关的答案有效。

Yen*_*ang 13

[]byte经过一番广泛的调查,我相信我已经发现了从Go 1.17开始获取 a 的最有效方法string(这是针对 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
Run Code Online (Sandbox Code Playgroud)

关于上述内容的其他不重要的有趣事实(很容易发生变化):3326B 的编译大小;内联成本为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
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

关于上述内容的其他不重要的有趣事实(很容易发生变化):3700B 的编译大小;内联成本为20;低于标准的逃逸分析:s leaks to {heap} with derefs=0.


修改 SliceHeader 的不安全版本

改编自努诺·克鲁塞斯的回答。这依赖于StringHeader和之间固有的结构相似性SliceHeader,因此从某种意义上来说它“更容易”被破坏。此外,它会暂时创建一个非法状态,其中cap(b)(being 0) 小于len(b)

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

对应组件(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
Run Code Online (Sandbox Code Playgroud)

其他不重要的细节:编译大小3636B、内联成本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)]
}
Run Code Online (Sandbox Code Playgroud)

相应的组件(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
Run Code Online (Sandbox Code Playgroud)

其他不重要的细节:编译后的大小3142B、 的内联成本9,以及正确的逃逸分析:s leaks to ~r1 with derefs=0

请注意runtime.panicSlice3Alen- 这是边界检查,检查是否len(s)0x7fff0000.


改进了指向数组的切片指针

这是我认为从 Go 1.17 开始最有效的方法。我基本上修改了接受的答案以消除边界检查,并找到了一个比 更有意义的常量 ( math.MaxInt32) 来使用0x7fff0000。使用MaxInt32保留 32 位兼容性。

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
Run Code Online (Sandbox Code Playgroud)

对应的组件(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
Run Code Online (Sandbox Code Playgroud)

其他不重要的细节:编译后的大小3188B、 的内联成本13以及正确的逃逸分析:s leaks to ~r1 with derefs=0



bla*_*een 12

Go 1.20(2023 年 2 月)

您可以使用unsafe.StringData来大大简化YenForYang 的答案

StringData 返回指向 str 底层字节的指针。对于空字符串,返回值未指定,并且可能为 nil。

由于 Go 字符串是不可变的,因此 StringData 返回的字节不得修改。

func main() {
    str := "foobar"
    d := unsafe.StringData(str)
    b := unsafe.Slice(d, len(str))
    fmt.Printf("%T, %s\n", b, b) // []uint8, foobar (byte is alias of uint8)
}
Run Code Online (Sandbox Code Playgroud)

Go 小技巧游乐场:https://go.dev/play/p/FIXe0rb8YHE?v= gotip

请记住,您不能分配给b[n]. 内存仍然是只读的。


icz*_*cza 9

将 a 的内容string作为 a[]byte获取而不进行复制一般只能使用unsafe,因为stringGo 中的 s 是不可变的,并且没有副本就可以修改 the 的内容string(通过更改字节切片的元素)。

所以使用unsafe,这就是它的样子(更正的,有效的解决方案):

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

此解决方案来自Ian Lance Taylor

原来,错误的解决方案是:

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

请参阅下面的Nuno Cruces 回答以进行推理。

测试它:

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

data = unsafeGetBytes("gopher")
fmt.Println(data, string(data))
Run Code Online (Sandbox Code Playgroud)

输出(在Go Playground上试试):

[104 105] hi
[103 111 112 104 101 114] gopher
Run Code Online (Sandbox Code Playgroud)

但是:你写你想要这个是因为你需要性能。您还提到要压缩数据。请知道压缩数据(使用gzip)需要更多的计算,而不仅仅是复制几个字节!使用它你不会看到任何明显的性能提升!

相反,当您想将strings写入an 时io.Writer,建议通过io.WriteString()函数执行此操作,如果可能,该函数将在不复制 the 的情况下执行此操作string(通过检查和调用WriteString()方法,如果存在则最有可能比复制 更好string)。详情请参见ResponseWriter.Write 和 io.WriteString 的区别是什么?

还有一些方法可以访问 a 的内容string而不将其转换为[]byte,例如索引,或使用编译器优化副本的循环:

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

另见相关问题:

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

在 go 中使用从 []byte 到 string 的不安全转换可能产生的后果是什么?

Go 中的字符串和 []byte 有什么区别?

Go 中别名类型之间的转换是否会创建副本?

内部类型转换是如何工作的?相同的内存利用率是多少?

  • @RFC7676 除非保留对字符串的引用,否则不能保证返回的切片指向有效的内存区域。由于您的示例没有这样做,因此允许在“unsafeGetBytes()”调用之后立即从内存中“擦除”字符串。请看【在Go中,变量什么时候会变得不可达?】(/sf/ask/2631204761/#37591282)一“明显” “解决方案是使用`runtime.KeepAlive()`。总而言之,尽量远离“unsafe”包,只有在没有其他选择的情况下才使用。 (2认同)

Nun*_*ces 6

接受的答案现在有一个更好的、权威的、来自 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
}
Run Code Online (Sandbox Code Playgroud)

一个字符串是:

type StringHeader struct {
    Data uintptr
    Len  int
}
Run Code Online (Sandbox Code Playgroud)

一个字节片转换成字符串可以“安全地”做的strings.Builder 做它

func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}
Run Code Online (Sandbox Code Playgroud)

这会将Data指针和Len从切片复制到字符串。

相反的转换并不“安全”,因为Cap没有设置为正确的值。

这是正确的代码,可以修复恐慌:

var buf = *(*[]byte)(unsafe.Pointer(&str))
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(str)
Run Code Online (Sandbox Code Playgroud)

也许:

var buf []byte
*(*string)(unsafe.Pointer(&buf)) = str
(*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(str)
Run Code Online (Sandbox Code Playgroud)

我应该补充一点,所有这些转换都是不安全的,因为字符串应该是不可变的,而字节数组/切片是可变的。

但是,如果您确定字节切片不会发生变异,则上述转换不会出现边界(或 GC)问题。


归档时间:

查看次数:

3321 次

最近记录:

4 年,4 月 前