使用runtime和unsafe做性能评估

举报
码乐 发表于 2024/03/18 10:29:00 2024/03/18
【摘要】 1 使用runtime和unsafe对比资源使用率。大多数场景,Go核心团队自用的机制,在runtime, reflect, sync, syscall都有广泛的使用。它支持程序员在go的安全类型中操作内存。想要使用unsafe包,就必须遵循 unsafe.Pointer的安全使用规则: 1 在常规操作下,go类型是安全的,但是使用unsafe包可以绕过这些类型安全的保护。 2 go兼容性...

1 使用runtime和unsafe对比资源使用率。

大多数场景,Go核心团队自用的机制,在runtime, reflect, sync, syscall都有广泛的使用。
它支持程序员在go的安全类型中操作内存。

想要使用unsafe包,就必须遵循 unsafe.Pointer的安全使用规则:

	1 在常规操作下,go类型是安全的,但是使用unsafe包可以绕过这些类型安全的保护。

	2 go兼容性不包括unsafe的任何承诺,非必要不要使用 unsafe包,尤其是 unsafe.Pointer

	3 uintptr 仅仅是一个整型值,即便它存储的是 内存对象的地址值,它对内存对象也起不到引用作用。

	4 使用unsafe包,请先牢记和理解 unsafe.Pointer 六个安全使用模式。

	5 使用了unsafe包,请使用 go vet 对代码进行 合规检查

c语言是静态语言,但不是类型安全的。 所谓类型安全是一块内存数据一旦被特定类型解释,该内存数据与该类型变量建立关联。
但是在c语言中,可以把 int 类型,使用c代码通过内存地址的置换,解释为字符串类型。

  • runtime包

Go 语言类型安全是建立在Go编译器的静态检查以及Go运行时 利用类型信息进行的运行时检查的。
在语法层面,为了实现常规类型安全,Go语言做了诸多限制.

而runtime包在上一节有详细内容介绍,它支持在运行时分析程序。
返回的内存分配器统计信息是在调用ReadMemStats时最新的。

调用ReadMemStats。这是与堆统计不同的。
它是最近完成的垃圾收集周期的一个快照。收集周期的快照。

runtime metrics 指标

metrics 提供了一个稳定的接口来访问实现定义的度量的稳定接口。
这个包类似于现有的函数如runtime.ReadMemStats和debug.ReadGCStats,但明显更通用。

这个包所定义的指标集可以随着运行时本身的发展而变化变化,也使得不同的Go实现有不同的变化,其相关的度量集可能不会相交。

以pprof可视化工具期望的格式写入运行时剖析数据。以格式化写入运行时剖析数据,并由 pprof 可视化工具进行处理。

  • 解析一个go程序

剖析Go程序的第一步是启用剖析功能。 支持对用标准测试构建的基准进行剖析。

此包内置在go test中。例如,下面的命令 在当前目录下运行基准,并将CPU和 内存配置文件到 cpu.prof 和 mem.prof。

	go test -cpuprofile cpu.prof -memprofile mem.prof -bench .

也有一个标准的HTTP接口来获取剖析数据。添加 以下一行将在/debug/pprof/下安装处理程序。

2 转换效率的对比实例

首先我们需要知道需要做什么对比,在go中转换类型的内置默认方法有强制转换如

	var i int = 64
	fmt.Printf("type:%T,value:%v \n",string(i), string(i))   //显示 64对应的字符
		输出: type:string,value:"@"

	us := strconv.FormatInt(int64(i), 10)
	fmt.Printf("type:%T,value:%v \n",string(us), string(us))   //显示64对应的十进制数字
		输出: typeus:string,value:"64"

我们实现go语言的类型string和[]byte之间的转换,和 基于 unsafe的类型转换,并进行benchmark基准性能测试。

而字节码到字符串的转换 byte to string 更消耗性能,

string类型变量是不可变类型,而[]byte为可变类型,当转换为[]byte时,go需要从系统申请一块新内存,

并将string类型变量值赋予这个新的内存,通过下面基于unsafe包的实现,不需要额外的内存复制:

转换后的[]byte变量与输入参数的string类型变量共享底层存储(并不能修改返回的切片就能改变原字符串。)

而将[]byte转换为string则简单一些,因为[]byte内部时一个三元组ptr,len,cap, 在string内部为 ptr,len。

只需要通过Pointer将[]byte内部重新解释为string的内部表示。

在这里,只是把 “this is golang!” 这字符串转string换为 []byte 或 把 []byte转换为字符串 string.

	//benchChange.go
	package main
	var benchStr string = "this is golang!"

使用go语言内置转换方式

		func B2String(b []byte) string {

			return string(b)
		}

		func S2Bytes(s string) []byte {

			return []byte(s)
		}

		func BenchmarkS2Bytes(b *testing.B) {
		var sbe string = benchStr
		for n := 0; n < b.N; n++ {
			S2Bytes(sbe)
			}
		}

		func BenchmarkB2String(b *testing.B) {
		var sbe []byte = []byte(benchStr)
		for n := 0; n < b.N; n++ {
			B2String(sbe)
			}
		}

使用unsafe避免内存分配和重新复制。

bytes转换为string

	func Bytes2String(b []byte) string {
		return *(*string)(unsafe.Pointer(&b))
	}

	func BenchmarkBytes2String(b *testing.B) {
		var sbe []byte = []byte(benchStr)
		for n := 0; n < b.N; n++ {
			Bytes2String(sbe)
		}
	}

string转换为bytes

	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 BenchmarkString2Bytes(b *testing.B) {
		var sbe string = benchStr
		for n := 0; n < b.N; n++ {
			String2Bytes(sbe)
		}
	}

将以上代码保存为 _unsafe_case.go。

使用go test执行case并且记录对应函数使用的cpu资源和内存mem资源。

	go test -cpuprofile cpu.prof -memprofile mem.prof -bench ./_unsafe_case.go
  • 效率对比: 内存使用数据

     go tool pprof cpu.prof
    

排名前200条使用记录:

    top 200 -cum 
	Showing nodes accounting for 3640.63kB, 100% of 3640.63kB total
	      flat  flat%   sum%        cum   cum%
	         0     0%     0%  1928.19kB 52.96%  runtime/pprof.(*profileBuilder).build
	         0     0%     0%  1928.19kB 52.96%  runtime/pprof.profileWriter
	         0     0%     0%  1712.44kB 47.04%  main.main
	         0     0%     0%  1712.44kB 47.04%  runtime.main
	         0     0%     0%  1712.44kB 47.04%  testing.(*M).Run
	         0     0%     0%  1414.69kB 38.86%  runtime/pprof.(*profileBuilder).appendLocsForStack       
	  512.10kB 14.07% 14.07%  1414.69kB 38.86%  runtime/pprof.(*profileBuilder).emitLocation
	 1184.27kB 32.53% 46.60%  1184.27kB 32.53%  runtime/pprof.StartCPUProfile
	         0     0% 46.60%  1184.27kB 32.53%  testing.(*M).before
	         0     0% 46.60%  1184.27kB 32.53%  testing/internal/testdeps.TestDeps.StartCPUProfile       
	  902.59kB 24.79% 71.39%   902.59kB 24.79%  compress/flate.NewWriter
	         0     0% 71.39%   902.59kB 24.79%  compress/gzip.(*Writer).Write
	         0     0% 71.39%   902.59kB 24.79%  runtime/pprof.(*profileBuilder).flush
	         0     0% 71.39%   528.17kB 14.51%  regexp.(*Regexp).MatchString (inline)
	         0     0% 71.39%   528.17kB 14.51%  regexp.(*Regexp).backtrack
	         0     0% 71.39%   528.17kB 14.51%  regexp.(*Regexp).doExecute
	         0     0% 71.39%   528.17kB 14.51%  regexp.(*Regexp).doMatch (inline)
	  528.17kB 14.51% 85.90%   528.17kB 14.51%  regexp.(*bitState).reset
	         0     0% 85.90%   528.17kB 14.51%  testing.newMatcher
	         0     0% 85.90%   528.17kB 14.51%  testing.runBenchmarks
	         0     0% 85.90%   528.17kB 14.51%  testing.simpleMatch.verify
	         0     0% 85.90%   528.17kB 14.51%  testing/internal/testdeps.TestDeps.MatchString
	         0     0% 85.90%   513.50kB 14.10%  runtime/pprof.(*profileBuilder).pbSample
	         0     0% 85.90%   513.50kB 14.10%  runtime/pprof.(*protobuf).length (inline)
	         0     0% 85.90%   513.50kB 14.10%  runtime/pprof.(*protobuf).uint64s
	  513.50kB 14.10%   100%   513.50kB 14.10%  runtime/pprof.(*protobuf).varint (inline)

可以看出,使用go语言层面的转换,cpu使用率有大幅提高。 内存使用率没有上榜不需对比。

根据生成的性能数据查看函数的资源使用率

	go tool pprof cpu.prof
  • 效率对比: cpu使用数据

排名前20的cpu资源使用率:

	top 20 -cum
	Showing nodes accounting for 4.01s, 97.33% of 4.12s total
	Dropped 52 nodes (cum <= 0.02s)
	      flat  flat%   sum%        cum   cum%
	         0     0%     0%      4.04s 98.06%  testing.(*B).runN
	         0     0%     0%      4.03s 97.82%  testing.(*B).launch
	     0.31s  7.52%  7.52%      1.27s 30.83%  _unsafe_case.BenchmarkS2Bytes    
	     0.35s  8.50% 16.02%      1.15s 27.91%  _unsafe_case.BenchmarkB2String   
	     0.14s  3.40% 19.42%      0.94s 22.82%  _unsafe_case.S2Bytes (inline)    
	     0.08s  1.94% 21.36%      0.80s 19.42%  _unsafe_case.B2String (inline)   
	     0.48s 11.65% 33.01%      0.80s 19.42%  runtime.stringtoslicebyte
	     0.36s  8.74% 41.75%      0.72s 17.48%  runtime.slicebytetostring
	     0.68s 16.50% 58.25%      0.68s 16.50%  runtime.memmove
	     0.51s 12.38% 70.63%      0.51s 12.38%  _unsafe_case.BenchmarkBytes2String
	     0.31s  7.52% 78.16%      0.31s  7.52%  _unsafe_case.BenchmarkGoFloat64bits
	     0.27s  6.55% 84.71%      0.27s  6.55%  _unsafe_case.BenchmarkGouint64bits
	     0.27s  6.55% 91.26%      0.27s  6.55%  _unsafe_case.BenchmarkString2Bytes
	     0.25s  6.07% 97.33%      0.25s  6.07%  _unsafe_case.BenchmarkFloat64bits         
	     0      0%    97.33%      0.03s  0.73%  runtime.mcall

可以看出 调用内置强制转换函数 S2Bytes 的 BenchmarkS2Bytes 使用了30%cpu
而调用内置强制转换函数 的 BenchmarkB2String 使用了 27%

而使用unsafe的转换方式,平均为 7%,至少提升了4倍转换效率。约为其内置强制转换方式的 26%.

3 小结

下一节我们介绍另一个优化性能的例子,获取10亿内的回文平方根数字时提升性能。

参考文档:

	arxiv.org/abs/2006.09973
	https://golang.org/pkg/runtime/debug/#SetTraceback.
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。