繁体   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