1 简介
本节专门使用多例程的方式充分利用cpu,以对比使用内置标准库unsafe包和语言层面的方式转换字符。
通过基准测试发现,对于string
转[]byte
,unsafe
包实现避免了内存复制,性能更优;
而[]byte
转string
两者差异较小。在float64
到uint64
的转换上,两种方法性能接近。
该例子的测试还展示了多CPU环境下转换函数的性能变化。
2 实例高效类型转换 性能对比
我们实现go语言的类型转换,和 基于 unsafe的类型转换,并进行benchmark基准性能测试。
string类型变量是不可变类型,而[]byte为可变类型,当转换为[]byte时,go需要从系统申请一块新内存,
并将string类型变量值赋予这个新的内存,通过下面基于unsafe包的实现,不需要额外的内存复制:
转换后的[]byte变量与输入参数的string类型变量共享底层存储(并不能修改返回的切片就能改变原字符串。)
而将[]byte转换为string则简单一些,因为[]byte内部时一个三元组ptr,len,cap, 在string内部为 ptr,len。
只需要通过Pointer将[]byte内部重新解释为string的内部表示。
在这里,只是把 "hello, world!" 这字符串转string换为 []byte 或 把 []byte转换为字符串 string
var benchStr string = "hello, world!"
func B2String(b []byte) string {
return string(b)
}
func S2Bytes(s string) []byte {
return []byte(s)
}
func BenchmarkB2String(b *testing.B) {
var sbe []byte = []byte(benchStr)
for n := 0; n < b.N; n++ {
B2String(sbe)
}
}
func BenchmarkS2Bytes(b *testing.B) {
var sbe string = benchStr
for n := 0; n < b.N; n++ {
S2Bytes(sbe)
}
}
func Bytes2String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func BenchmarkBytes2String(b *testing.B) {
var sbe []byte = []byte(benchStr)
for n := 0; n < b.N; n++ {
Bytes2String(sbe)
}
}
func BenchmarkString2Bytes(b *testing.B) {
var sbe string = benchStr
for n := 0; n < b.N; n++ {
String2Bytes(sbe)
}
}
3 float64 和 uint64 转换没有太大区别
float64 到 uint64的转换,由于没有内存的重新分配和复制操作, 使用go语言函数和 unsafe包没有太多区别。
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkFloat64bits
BenchmarkFloat64bits-2 1000000000 0.3591 ns/op
BenchmarkFloat64bits-2 1000000000 0.3238 ns/op
BenchmarkFloat64bits-4 1000000000 0.3271 ns/op
BenchmarkFloat64bits-4 1000000000 0.3104 ns/op
BenchmarkFloat64bits-8 1000000000 0.3144 ns/op
BenchmarkFloat64bits-8 1000000000 0.3318 ns/op
BenchmarkGoFloat64bits
BenchmarkGoFloat64bits-2 1000000000 0.3072 ns/op
BenchmarkGoFloat64bits-2 1000000000 0.2978 ns/op
BenchmarkGoFloat64bits-4 1000000000 0.3185 ns/op
BenchmarkGoFloat64bits-4 1000000000 0.3242 ns/op
BenchmarkGoFloat64bits-8 1000000000 0.2980 ns/op
BenchmarkGoFloat64bits-8 1000000000 0.3265 ns/op
BenchmarkGouint64bits
BenchmarkGouint64bits-2 1000000000 0.3019 ns/op
BenchmarkGouint64bits-2 1000000000 0.3115 ns/op
BenchmarkGouint64bits-4 1000000000 0.3227 ns/op
BenchmarkGouint64bits-4 1000000000 0.2969 ns/op
BenchmarkGouint64bits-8 1000000000 0.3016 ns/op
BenchmarkGouint64bits-8 1000000000 0.3229 ns/op
PASS
ok command-line-arguments 6.546s
4 多cpu执行和分析结果
指令执行
go test -v -count 2 -bench . bytestr_change_bench_test.go -cpu 2,4,8 >l.txt
在linux 执行结果如下:
goos: linux
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
使用go函数,转换字符 hello world, 从 byte 到string
BenchmarkB2String
BenchmarkB2String-2 241006406 4.974 ns/op
BenchmarkB2String-2 246260128 5.265 ns/op
BenchmarkB2String-4 204571192 5.306 ns/op
BenchmarkB2String-4 227578519 5.266 ns/op
BenchmarkB2String-8 224398243 5.691 ns/op
BenchmarkB2String-8 230737006 5.544 ns/op
使用go函数 转换字符 hello world, 从 string到byte
BenchmarkS2Bytes
BenchmarkS2Bytes-2 240975625 5.047 ns/op
BenchmarkS2Bytes-2 180348247 5.797 ns/op
BenchmarkS2Bytes-4 233383180 4.889 ns/op
BenchmarkS2Bytes-4 257518407 5.167 ns/op
BenchmarkS2Bytes-8 245337022 6.379 ns/op
BenchmarkS2Bytes-8 124589875 8.548 ns/op
使用usafe包转换字符 hello world, 从 byte 到string
BenchmarkBytes2String
BenchmarkBytes2String-2 1000000000 1.035 ns/op
BenchmarkBytes2String-2 1000000000 1.009 ns/op
BenchmarkBytes2String-4 1000000000 0.7254 ns/op
BenchmarkBytes2String-4 1000000000 0.8996 ns/op
BenchmarkBytes2String-8 1000000000 0.7063 ns/op
BenchmarkBytes2String-8 1000000000 0.7204 ns/op
使用usafe包转换字符 hello world, 从 string到byte
BenchmarkString2Bytes
BenchmarkString2Bytes-2 1000000000 0.4363 ns/op
BenchmarkString2Bytes-2 1000000000 0.3780 ns/op
BenchmarkString2Bytes-4 1000000000 0.3678 ns/op
BenchmarkString2Bytes-4 1000000000 0.3154 ns/op
BenchmarkString2Bytes-8 1000000000 0.3109 ns/op
BenchmarkString2Bytes-8 1000000000 0.3334 ns/op
PASS
ok command-line-arguments 32.022s
这个例子中通过 -cpu 2,4,8 指令选项告知 go test 将每个性能基准测试函数分别在 GOMAXPROCS 等于 2,4,8的场景各运行一次。
从输出结果看,我们可以很容易看出不同被测试函数的性能随着 GOMAXPROCS增大后的性能变化。
每次执行基准测试,go test 都会启动 GOMAXPROCS 数量的新 goroutine,这些 goroutine 共同执行 b.N 次循环。
每个goroutine 尽量均衡分担循环次数。
这里输出的内存分配信息告诉我们,op(代表每次循环操作) 每一次执行 该值表示 函数在基准测试中 for 循环每次平均执行时间。
BenchmarkString2Bytes-8 1000000000 0.3334 ns/op
也就是函数 BenchmarkString2Bytes 的例程 -8 每次for循环0.3334 ns 执行时间。
BenchmarkS2Bytes-8 124589875 8.548 ns/op
也就是函数 BenchmarkS2Bytes 的例程 -8 每次for循环 8.548 ns 平均执行时间。
在不同平台执行速度相差不大。
但是从结果直观地看,go语言函数转换和unsafe包转换都在 ns 级别。
使用unsafe的转换效率最高快 25倍,也就是8.548 : 0.3334。
5 小结
得到的结果是,在特定场合比如需要内存分配和复制的场景,内置库可以大幅度提高转换效率。
这是建立在熟练操作和理解其过程的原理的基础上。
后面几个章节,将从更多例子和原理尝试解释它。