与日俱进,在 Go 1.20 中这种高效转换的方式又变了
在 Go 1.19 的开发中, string.SliceHeader
和 string.StringHeader
经历了一个生死存亡的争斗,这两个类型一度被标记为弃用( deprecated ),但是这两个类型经常用在 slice of byte 和 string 高效互转的场景中,如果被标记为弃用,但是目前还没有可替代的方法,所以这两个类型又把弃用标记去掉了,如无意外,它们也会在 Go 1.20 再次被标记为弃用。
byte slice 和 string 的转换优化
直接通过强转 string(bytes)
或者 []byte(str)
会带来数据的复制,性能不佳,所以在追求极致性能场景,我们会采用『骇客』的方式,来实现这两种类型的转换,比如k8s采用下面的方式:
https://github.com/kubernetes/apiserver/blob/706a6d89cf35950281e095bb1eeed5e3211d6272/pkg/authentication/token/cache/cached_token_authenticator.go#L263-L271
// toBytes performs unholy acts to avoid allocations func toBytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&s)) }// toString performs unholy acts to avoid allocations func toString(b []byte) string {return *(*string)(unsafe.Pointer(&b)) } |
更多的采用下面的方式(rpcx也采用下面的方式):
func SliceByteToString(b []byte) string {return *(*string)(unsafe.Pointer(&b)) }func StringToSliceByte(s string) []byte {x := (*[2]uintptr)(unsafe.Pointer(&s))h := [3]uintptr{x[0], x[1], x[1]}return *(*[]byte)(unsafe.Pointer(&h)) }
甚至,标准库也采用这种方式:
https://github.com/golang/go/blob/82f902ae8e2b7f7eff0cdb087e47e939cc296a62/src/strings/clone.go
func Clone(s string) string {if len(s) ==0 {return ""}b := make([]byte, len(s))copy(b, s)return *(*string)(unsafe.Pointer(&b)) } |
因为 slice of byte 和 string 数据结构类似,所以我们可以可以使用这种『骇客』的方式强转。这两种类型的数据结构在 reflect
包中有定义:
type SliceHeader struct {Data uintptrLen intCap int } type StringHeader struct {Data uintptrLen int }
Slice
比 String
多一个 Cap
字段,它们的数据通过一个数组存储,这两个结构的 Data
存储了指向这个数组的指针。
Go 1.20 的新的方式
很多项目中都使用上面的方式进行性能提升,但是这是通过 unsafe
实现的,有相当的风险,因为强转之后,slice可能会做一些变动,导致相关的数据被覆盖了或者被回收了,也经常会出现一些意想不到的问题,我在使用这种方式做RedisProxy的时候,也犯过类似的错误,我当时还以为是标准库出错了呢。
因此, Go官方准备在 1.20 中把这两个类型 SliceHeader
和 StringHeader
废弃掉,避免大家的误用。
废弃就废弃吧,但是也得提供相应的替代方法才行。这不,在 Go 1.12中,增加了几个方法 String
、 StringData
、 Slice
和 SliceData
,用来做这种性能转换。
- func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType: 返回一个Slice,它的底层数组自ptr开始,长度和容量都是len
- func SliceData(slice []ArbitraryType) *ArbitraryType:返回一个指针,指向底层的数组
- func String(ptr *byte, len IntegerType) string: 生成一个字符串,底层的数组开始自ptr, 长度是len
- func StringData(str string) *byte: 返回字符串底层的数组
这四个方法看起来很原始很底层。
这个提交是由cuiweixie提交的。因为涉及到很基础很底层的实现,而且又是可能被广泛使用的方法,所以大家review起来特别的仔细,大家可以围观: go-review#427095 。
甚至,这个修改都惊动了蛰伏多月的Rob Pike大佬,他老人家询问为啥只有实现连注释文档都没有呢: #54858 ,当然原因是这个功能还在开发和review之中,不过可以看出Rob Pike很重视这个修改。
cuiweixie 甚至还修改了标准库里面一些 写法 ,使用他提交的unsafe中的这四个方法。
性能测试
虽然cuiweixie的提交还没有被merge到主分支,还存在一些变数,但是我发现使用gotip能使用这几个方法了。 我理解的是gotip适合master分支保持一致的,难道不是么?
不管怎样,先写个benchmark:
var L =1024 *1024 var str = strings.Repeat("a", L) var s = bytes.Repeat([]byte{'a'}, L)var str2 string var s2 []bytefunc BenchmarkString2Slice(b *testing.B) {for i :=0; i < b.N; i++ {bt := []byte(str)if len(bt) != L {b.Fatal()}} }func BenchmarkString2SliceReflect(b *testing.B) {for i :=0; i < b.N; i++ {bt := *(*[]byte)(unsafe.Pointer(&str))if len(bt) != L {b.Fatal()}} }func BenchmarkString2SliceUnsafe(b *testing.B) {for i :=0; i < b.N; i++ {bt := unsafe.Slice(unsafe.StringData(str), len(str))if len(bt) != L {b.Fatal()}} }func BenchmarkSlice2String(b *testing.B) {for i :=0; i < b.N; i++ {ss := string(s)if len(ss) != L {b.Fatal()}} }func BenchmarkSlice2StringReflect(b *testing.B) {for i :=0; i < b.N; i++ {ss := *(*string)(unsafe.Pointer(&s))if len(ss) != L {b.Fatal()}} }func BenchmarkSlice2StringUnsafe(b *testing.B) {for i :=0; i < b.N; i++ {ss := unsafe.String(unsafe.SliceData(s), len(str))if len(ss) != L {b.Fatal()}} }
实际测试结果:
➜ strslice gotip test -benchmem -bench . goos: darwin goarch: arm64 pkg: github.com/smallnest/study/strslice BenchmarkString2Slice-8 18826 63942 ns/op 1048579 B/op 1 allocs/op BenchmarkString2SliceReflect-8 1000000000 0.6498 ns/op 0 B/op 0 allocs/op BenchmarkString2SliceUnsafe-8 1000000000 0.8178 ns/op 0 B/op 0 allocs/op BenchmarkSlice2String-8 18686 65864 ns/op 1048580 B/op 1 allocs/op BenchmarkSlice2StringReflect-8 1000000000 0.6488 ns/op 0 B/op 0 allocs/op BenchmarkSlice2StringUnsafe-8 1000000000 0.9744 ns/op 0 B/op 0 allocs/op
可以看到,不通过『骇客』的方式,两种类型强转耗时非常巨大,如果采用 reflect
的方式,性能提升大大改观。
如果采用最新的 unsafe
包的方式,性能也能大大提高,虽然耗时比 reflect
略有增加,可以忽略。
与日俱进,在 Go 1.20 中这种高效转换的方式又变了相关推荐
- 谷医堂与日俱进!谷医堂优化产品和提升服务两不误
"高阿姨,最近气色变好了很多啊","是啊是啊,我身边好几个朋友都这么说呢",在谷医堂门店,总能听到这样的对话. 近年来,女性朋友对于自身健康管理的投入逐渐提高. ...
- 12月18日云栖精选夜读 | Java 中创建对象的 5 种方式!...
作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有5种创建对象的方式,下面给出它们的 ...
- 与“十“俱进 阿里数据库运维10年演进之路
与"十"俱进 阿里数据库运维10年演进之路 原文:与"十"俱进 阿里数据库运维10年演进之路 阿里巴巴集团拥有超大的数据库实例规模,在快速发展的过程中我们在运维 ...
- 仙剑5手游服务器维护,仙剑奇侠传手游5月20日活动有哪些?5.20日例行维护时间...
仙剑奇侠传手游5月20日活动有哪些?5.20日例行维护时间. 尊敬的仙剑玩家,为了给您提供更好的游戏体验,我们将于5月20日14:00-16:00进行安卓全区例行维护,16:00-18:00进行iOS ...
- lol日服一直显示重新连接服务器,lol手游日服登录不了怎么办 日服进不去解决方法...
lol手游日服为什么会进不去?这款手游已经在很多地区都进行了公测,小伙伴们对于日服进不去的问题也是反映了很多次,但是一直没有得到解决,究竟如何才能进入游戏?接下来就让小编来给大家提供解决问题的方案吧. ...
- 荒野行动PC服务器维护,荒野行动PC版1月26日为什么进不去 荒野行动PC版1月26日维护时间公告...
荒野行动PC版1月26日为什么进不去了?荒野行动PC版进行了一波大更新,本来上午还好好的,很多玩家下午再登陆游戏之后就发现游戏处于维护阶段,这是为什么呢?荒野行动PC版1月26日什么时候才能维护好呢? ...
- 进神经网络的学习方式(译文)----中
过匹配和规范化 诺贝尔奖得主美籍意大利裔物理学家恩里科·费米曾被问到他对一个同僚提出的尝试解决一个重要的未解决物理难题的数学模型.模型和实验非常匹配,但是费米却对其产生了怀疑.他问模型中需要设置的自由 ...
- c语言运行不显示图片,为何加载烟花就换了一句,将图片加载进资源,结果运行中烟花不显示...
为何加载烟花就换了一句,将图片加载进资源,结果运行中烟花不显示 为何将图片装载在资源中编译没任何错误,程序也可基本运行,资源中的图片不工作 #include //标准的输入输出头文件 #inc ...
- ACMNO.42 C语言-第几天 定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题。利用结构体的在最下面
题目描述 定义一个结构体变量(包括年.月.日).计算该日在本年中是第几天,注意闰年问题. 输入 年月日 输出 当年第几天 样例输入 2000 12 31 样例输出 366 来源/分类 C语言 题目截图 ...
最新文章
- Pacbio 数据相关的几个重要概念
- Ansible第一篇:基础
- 河南计算机程序大赛,我院成功举办河南省第十一届ACM大学生程序设计竞赛
- 史上最硬核的Linux依赖问题解决方案
- 盛语小智教育机器人是骗人的_武清区人民检察院未检工作室,开展普法机器人进校园宣讲活动...
- python观察日志(part18)--遍历文件夹下文件并判断后缀
- HTML5 canvas图形库 RGraph【转】
- 全球CT影像20秒诊断,阿里云为新冠AI辅助诊断系统加速 | 凌云时刻
- ERP系统多少钱一套?不同情况详情分析告诉你!
- netty原理简介及服务端、客户端详细代码
- 来上海度过的第一个五一,我去了哪些地方?
- Mac访问微软远程桌面Microsoft Remote Desktop For Mac
- Java 安全 后端返回文件流
- QT之鼠标点击事件学习
- SQL(进阶实战05)
- java操作word,自动更新目录/域
- 有人用这个android控制我的手机,用这个软件,竟可以随便控制别人的手机
- 一条狗的死亡,引发3亿网友愤怒!希望这条黑科技 “汪星人” 能从小培养人的爱心 | 钛空智慧星球推荐
- 备份vmware虚拟机,failed. Error 2 (Memory allocation failed. Out of memory.) (DiskLib error 802
- 解决local variable 'has_fav_course' referenced before assignment(Python)