最近写一个 golang 的工具包时,涉及到反复在 string 和 []byte 之间来回转换。这给了我一个机会了解转换时底层发生的事情。
结论先行
- string 和 []byte 互转都涉及底层数据复制;可以通过 unsafe 强制转换绕过复制提高性能。
- string 类型的底层数组可能放在常量区(字面量初始化)也可能动态分配;无论哪一种,当以 string 类型出现时底层数据都是不可修改的(避免影响其他引用),string 的修改实际上是指向重新生成的底层数组。
- 当以 []byte 类型出现时,可以修改具体某一个 byte 的值;不过如果是从指向常量区的 string 通过 unsafe 转换而来,尝试修改时会产生不可恢复的 runtime error。
- 这也是为什么这个包叫 unsafe:绕过类型检查强行转换,绕过了底层数据复制,提高性能同时也失去了检查和复制的保护,需要调用方自行确认不会出错。
+
连接会复制内存,strings.Split()
直接在原串做切片…具体不同实现要以源码为准。- 即使 string 是动态分配的内容,也不建议修改对应的 []byte,可能会引起引用同一块内容的其他 string 的异常—— 除非你能确保没有别的地方引用它。
- 实际调用中碰到了 对 A 串做 unsafe 转换,结果完全无关的 B 串出现切片时数组越界;A 串改为普通转换就好了。暂时没能找到原因,保险起见放弃使用 unsafe,改为用
json.RawMessage
多封装一层。本次研究权当学习了。
情景
具体来说,是一个缓存相关的封装包:把对象 json.Marshal()
之后得到的 []byte 放到缓存;或者反过来,取出 []byte 交给 json.Unmarshal()
。无论正反方向的调用,输入输出都是 []byte,本没有 string 的事。
偏偏这中间为了实现某些功能,需要往 marshal 之后的 json 字符串上追加(存的时候)或提取(读的时候)某些信息。追加还好办,可以把追加的内容一起变成 []byte 之后 append()
;但提取需要基于字符串的语义,不得不先转成 string,解析完再转回来。
我对这中间的开销产生了兴趣。
unsafe
写 demo 对比地址之后,可以确定每次转换,string 和 []byte 的地址都有变化。
但是这只能确定 string 和 slice ([]byte 的 slice) 的结构体发生了复制,结构体指向的底层 byte array 是否有发生复制无从得知。
通过查阅网上的讨论以及源码,进一步确认,底层的 array 也发生的复制。既然发生了深复制,那么一定是有额外开销的,只是多和少的差别。
通过 unsafe
包,可以强制直接转换来绕过复制的开销。实际上 strings.Builder
就是这样做的。
|
|
Benchmark
那究竟两种转换方式差别有多大呢?
跑一个 Benchmark:
|
|
测试结果对比如下
|
|
我用不同数据测过几次,上面只是其中一次。用较短的字符串测试时,大概是 20 倍的差距;当字符串越来越长,差距越来越明显。所以当反复转换较长的字符创时,可以考虑用 unsafe。
字面量常量
网友提到这个转换只能对动态生成的字符串用,但是我上面的测试用了字面量常量,却没有报错呢?
有什么办法可以让它报错呢?我想差别是在于读和写。上面虽然对字面量常量做了 unsafe 转换,但是转换之后得到的结果并没有尝试写入。
|
|
果然报错了
|
|
改为动态生成的字符串看看。怎么生成?可以用 strings.Builder
。
|
|
这回就不报错了
|
|
其他情况
concat 运算符 +
如果一个字符串是通过连接符 +
得到的呢?
它的实现在 src/runtime/string.go
里,可以看到做了内存复制。所以无论连接前的字符串是动态分配的还是字符串常量,连接之后得到的结果都是动态分配的。
strings.Split()
类似的,如果字符串是分解得到的子串呢?
同样可以看源码,Split 实际上并没有生成字符串,而是在原串的基础上做了切片。所以修改是否会报错,视乎原串是常量还是动态分配的内存。
其他情况就不一一尝试了。总的来说,各种情况都有,需要看源码核实。不过确认是动态分配的串也只是避免 runtime error 造成程序中断而已;但即使不报错,也应该避免修改 unsafe 转换得到的 []byte,这会导致其他指向同一地址的 string 有意外的结果—— 除非你确定这是对底层 array 唯一的引用 。
参考资料
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
详情请点击查看协议具体内容。