使用unsfae和运行时库分析性能

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 【5月更文挑战第19天】该文探讨了Go语言中使用`runtime`和`unsafe`包对资源使用率的影响。`unsafe`包允许绕过类型安全,但不保证兼容性,使用时需谨慎。`runtime`包提供运行时分析,包括内存分配统计。总结中提到,`unsafe`转换在某些情况下能提升约4倍效率。接下来的内容会涉及更多性能优化策略。

简介

文章通过示例展示unsafe和runtime工具在字符串与字节切片转换中的作用,unsafe的转换方法相比内置转换更高效,减少了CPU使用率。通过go testpprof工具进行了性能基准测试和分析。

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

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

simple.png

想要使用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.
目录
相关文章
|
9月前
|
缓存 算法 NoSQL
如何优化Java应用程序的性能
无论是开发大型企业应用程序还是小型工具,Java一直是一个受欢迎的编程语言。然而,随着应用程序规模的增长和用户需求的变化,性能成为了一个关键问题。本篇博客将介绍一些优化Java应用程序性能的方法。
106 1
|
1月前
LabVIEW配置可执行程序以运行更高版本的运行引擎
LabVIEW配置可执行程序以运行更高版本的运行引擎
13 1
|
1月前
|
算法 编译器 C语言
【C/C++ 编译器的差异化】C++标准库在GCC和VS之间的表现差异
【C/C++ 编译器的差异化】C++标准库在GCC和VS之间的表现差异
233 1
|
1月前
|
编译器 Linux C语言
Valgrind兼容性解析:从核心依赖到错误诊断
Valgrind兼容性解析:从核心依赖到错误诊断
111 0
|
1月前
|
缓存 编译器 程序员
C/C++编译器全局优化技术:全局优化是针对整个程序进行的优化,包括函数之间的优化
C/C++编译器全局优化技术:全局优化是针对整个程序进行的优化,包括函数之间的优化
40 0
|
1月前
|
缓存 监控 编译器
即时编译(JIT):从源代码到高效执行的神奇之旅(下)
即时编译(JIT):从源代码到高效执行的神奇之旅(下)
|
1月前
|
缓存 监控 Java
即时编译(JIT):从源代码到高效执行的神奇之旅(上)
即时编译(JIT):从源代码到高效执行的神奇之旅(上)
|
9月前
|
缓存 监控 算法
优化Java应用程序性能的技巧和策略
在开发Java应用程序时,优化性能是一个重要的考虑因素。一个高效的应用程序可以提供更好的用户体验、更低的资源消耗和更高的可伸缩性。本文将介绍一些优化Java应用程序性能的常见技巧和策略,帮助开发人员充分发挥Java的潜力。
140 1
|
存储 缓存 监控
聊聊JIT是如何影响JVM性能的
我们知道Java虚拟机栈是线程私有的,每个线程对应一个栈,每个线程在执行一个方法时会创建一个对应的栈帧,栈帧负责存储局部变量变量表、操作数栈、动态链接和方法返回地址等信息,每个方法的调用过程,相当于栈帧在Java栈的入栈和出栈过程但是栈帧的创建是需要耗费资源的,尤其是对于 Java 中常见的 getter、setter 方法来说,这些代码通常只有一行,每次都创建栈帧的话就太浪费了。另外,Java 虚拟机栈对代码的执行,采用的是字节码解释执行的方式,考虑到下面这段代码,变量 a 声明之后,就再也不被使用,要是按照字节码指令解释执行的话,就要做很多无用功。另外,我们知道垃圾回收器回收的目标区域主要
|
算法 Java 测试技术
官方文档:Android应用程序运行的性能设计
  Android应用程序运行的移动设备受限于其运算能力,存储空间,及电池续航。由此,它必须是高效的。电池续航可能是一个促使你优化程序的原因,即使他看起来已经运行的足够快了。由于续航对用户的重要性,当电量耗损陡增时,意味这用户迟早会发现是由于你的程序。
839 0