String 如何在没有内存拷贝的情况下从字符串中获取字节片
我已经读过关于“从String 如何在没有内存拷贝的情况下从字符串中获取字节片,string,performance,go,slice,String,Performance,Go,Slice,我已经读过关于“从[]字节到字符串的无拷贝转换”的文章 我想知道是否有一种方法可以在没有内存拷贝的情况下将字符串转换为字节片 我正在写一个处理terra字节数据的程序,如果每个字符串在内存中复制两次,就会减慢进程。我不关心易变/不安全,只关心内部使用,我只需要尽可能快的速度 例如: var s string // some processing on s, for some reasons, I must use string here // ... // then output to a wr
[]字节
到字符串
的无拷贝转换”的文章
我想知道是否有一种方法可以在没有内存拷贝的情况下将字符串转换为字节片
我正在写一个处理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
所以问题是:有没有办法防止内存复制?我知道也许我需要不安全的包裹,但我不知道怎么做。我已经搜索了一段时间,到目前为止没有答案,因此显示的相关答案都不起作用。将
字符串的内容作为[]字节获取,而通常不进行复制,只有使用才能实现,因为Go中的字符串是不可变的,如果没有副本,就可以修改字符串的内容(通过更改字节片的元素)
因此,使用不安全的
,这就是它的样子(更正,工作解决方案):
此解决方案来自
最初,错误的解决方案是:
func unsafeGetBytesWRONG(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s)) // WRONG!!!!
}
有关推理,请参见下文
测试它:
s := "hi"
data := unsafeGetBytes(s)
fmt.Println(data, string(data))
data = unsafeGetBytes("gopher")
fmt.Println(data, string(data))
输出(在上尝试):
但是:你写了你想要这个,因为你需要性能。您还提到要压缩数据。请知道,压缩数据(使用gzip
)需要比复制几个字节多得多的计算量!通过使用此选项,您不会看到任何明显的性能提升
相反,当您想将字符串
写入时,建议通过函数执行,如果可能,该函数将不复制字符串
(通过检查并调用WriteString()
方法,如果存在该方法,则很可能比复制字符串
做得更好)。有关详细信息,请参阅
还有一些方法可以访问字符串的内容
,而无需将其转换为[]字节
,例如索引,或使用编译器优化副本的循环:
s := "something"
for i, v := range []byte(s) { // Copying s is optimized away
// ...
}
另请参见相关问题:
我通过以下方式实现了目标:
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
这些变量共享相同的内存。接受的答案是错误的,可能会产生注释中提到的恐慌@RFC。@icza对GC和keep alive的解释是错误的
容量为零(甚至是任意值)的原因更加平淡无奇
一个切片是:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
字符串是:
type StringHeader struct {
Data uintptr
Len int
}
将字节片转换为字符串可以像strings.Builder
那样“安全地”完成:
这将把数据
指针和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)问题。无拷贝转换需要使用不安全的包,并且非常不鼓励使用,因为它会给您留下一个可变字符串(因此“不安全”)。[]字节(str)
在几乎所有情况下都足够快。如果不是()的话,你应该首先考虑为什么有必要这样做(也许你可以使用字节切片?或字符串.Builder?),而不是跳转到不安全的包。@Peter我正在编写一个处理terra字节数据的程序,如果每个字符串在内存中复制两次,它会减慢进度。我不在乎易变/不安全,只关心内部使用,我只需要尽可能快的速度。@shawn光是这一点并不能保证使用unsafe
。有一些方法可以在不获取[]字节
副本的情况下处理字符串的内容(例如索引、循环、字符串读取器等)。@shawn如果使用gzip
压缩数据,那么复制字符串所需的计算量要多得多。在不复制的情况下获取string
内容不会给您带来明显的性能提升。恐慌:运行时错误:片容量超出范围[:52]0@RFC7676是否愿意共享产生该错误的代码?我共享的代码适用于Go Playery.Go版本go1.14.1 linux/amd64@RFC7676。除非保留对字符串的引用,否则无法保证返回的切片指向有效内存区域。因为您的示例没有这样做,所以在调用unsafeGetBytes()
之后,允许激进的gc从内存中“擦除”字符串。请看一个“明显”的解决方案是使用runtime.KeepAlive()
。总之,尽量远离包装不安全的,只有在没有其他选择时才使用。转换不正确。调用data=append(数据“!”)
会导致崩溃。它应该是:return*(*[]字节)(unsafe.Pointer(&struct{string;int}{s,len(s)}))
你说得对,谢谢你的解释。我还修复了我的代码(保留了原来的错误代码),展示了Ian Lance Taylor有趣的解决方案。
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
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)