现有Go程序性能优化分析及其实现 | 青训营

59 阅读6分钟

一、前言

Go语言作为一种高性能的编程语言,具备了很多优秀的特性和工具,帮助开发人员进行程序性能优化分析。接下来,我们将尝试如何使用这些工具来分析和优化Go程序的性能。

二、go test -bench使用

go test命令是Go语言的测试工具,它可以通过添加-bench标志来运行基准测试。基准测试是一种用于测量代码性能的测试方法。通过运行基准测试,我们可以获得代码的执行时间、内存分配等详细信息。

下面我们创建文件名为 benchmark_test.go 的测试示例,然后运行代码,运行结果为输出性能统计信息。

  1. fibonacci函数是我们要进行基准测试的功能代码。
  2. BenchmarkFibonacci函数是我们的基准测试函数。它使用testing.B类型的参数来访问基准测试相关的功能。在这个示例中,我们调用fibonacci函数作为要进行基准测试的代码,并在循环中重复调用它。
  3. main函数中,我们使用testing.Benchmark函数来运行基准测试函数。
package main
​
import (
    "fmt"
    "testing"
)
​
// 要进行基准测试的函数
func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}
​
// 基准测试函数
func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fibonacci(10) // 调用要进行基准测试的函数
    }
}
​
func main() {
    fmt.Println("Running benchmark...")
    testing.Benchmark(BenchmarkFibonacci)
}

运行结果,下图表示程序通过了基准测试

  • BenchmarkFibonacci-8:表示运行基准测试函数 BenchmarkFibonacci 的结果。-8 表示使用 8 个 CPU 核心进行测试。
  • 4275781:表示总共执行了 4275781 次操作(在 b.N 循环中)。
  • 263.3 ns/op:表示每个操作平均耗时 263.3 纳秒。

image-20230829214332001

三、go tool pprof使用

go tool pprof是一个命令行工具,用于分析和可视化由go profiler生成的分析报告。它可以显示函数的CPU使用情况、内存分配情况等,并提供了交互式界面来帮助我们深入了解代码的性能问题。

1、代码示例

下面代码是创建一个数字切片,并对其执行了简单的操作,这里使用了net/http/pprof包来提供性能分析的Web接口。通过调用http.ListenAndServe()函数在端口8080上启动一个HTTP服务器,并将其放在一个goroutine中以便与主程序并发运行。

package main
​
import (
    "fmt"
    "math/rand"
    "net/http"
    _ "net/http/pprof"
)
​
func GenerateSlice(n int) []int {
    s := make([]int, n)
    for i := 0; i < n; i++ {
        s[i] = rand.Intn(n)
    }
    return s
}
​
func SumSlice(s []int) int {
    sum := 0
    for _, v := range s {
        sum += v
    }
    return sum
}
​
func main() {
    go func() {
        http.ListenAndServe(":8080", nil)
    }()
    for {
        s := GenerateSlice(1000000)
        fmt.Println(SumSlice(s))
    }
}

2、性能分析

要访问性能分析数据,请在浏览器中输入http://localhost:8080/debug/pprof/。这将显示可用的性能分析选项。例如,要查看CPU配置文件,请访问http://localhost:6060/debug/pprof/profile

image-20230829231725795

接下来我们使用下面命令来获取CPU的profile:

wget http://localhost:8080/debug/pprof/profile?seconds=30 -O cpu.pprof

然后,分析CPU数据:

go tool pprof -http=:8081 cpu.pprof

注意:在Windows上打开网页可能会提示 Could not execute dot; may need to install graphviz.,这个时候我们需要去安装 graphviz 组件,下载地址,我们选择Windows版下载,然后安装,注意安装工程中勾选环境变量,然后到graphviz的安装目录里面的bin下打开管理员cmd执行 dot -c 来生成配置文件

image-20230829235644360

得到火焰图展示

image-20230830000615103

3、分析结论

在上面代码中,存在以下几个潜在问题:

  1. GenerateSlice()函数使用rand.Intn(n)生成随机数。在每次循环迭代中都调用该函数可能会导致较大的开销,因为它需要进行随机数生成。
  2. SumSlice()函数对切片进行遍历来计算总和。这可能是一个耗时的操作,特别是当切片很大时。

4、优化代码

优化代码过程:

  • 添加了randomSlice变量来缓存随机数切片。
  • 引入了randomSliceMutex互斥锁来保护对randomSlice的并发访问。
  • GenerateSlice()函数首先检查缓存中的随机数切片,如果已存在且长度与所需长度相同,则直接返回缓存值。否则,生成新的随机数切片并更新缓存。
  • main()函数中,使用了sync.WaitGroup和并行化的方式启动了goroutine。

通过这样的修改,随机数切片将在第一次生成后被缓存,并在后续调用中被复用,从而避免了重复生成的开销。请注意,在多个goroutine同时访问和更新randomSlice时,使用互斥锁是为了确保安全的并发访问。

package main
​
import (
   "math/rand"
   "net/http"
   _ "net/http/pprof"
   "sync"
)
​
var randomSlice []int
var randomSliceMutex sync.Mutex
​
func GenerateSlice(n int) []int {
   randomSliceMutex.Lock()
   defer randomSliceMutex.Unlock()
​
   if randomSlice != nil && len(randomSlice) == n {
      return randomSlice
   }
​
   s := make([]int, n)
   for i := 0; i < n; i++ {
      s[i] = rand.Intn(n)
   }
​
   randomSlice = s
   return s
}
​
func SumSlice(s []int) int {
   sum := 0
   for _, v := range s {
      sum += v
   }
   return sum
}
​
func main() {
   go func() {
      http.ListenAndServe(":8080", nil)
   }()
​
   var wg sync.WaitGroup
​
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         for {
            s := GenerateSlice(1000000)
            SumSlice(s)
         }
      }()
   }
​
   wg.Wait()
}

现在我们继续重复上面性能分析的步骤,获取到我们CPU的profile,对CPU数据进行分析,然后进行火焰图展示

image-20230830001723570

在我们日常的开发中,可以通过pprof很快的找出程序中的性能瓶颈,并对代码进行优化。

四、性能优化总结

  1. 进行性能分析,使用Go的内置性能分析工具来检测瓶颈和性能问题。
  2. 确定性能指标,明确要改进的性能指标,以便衡量优化效果。
  3. 使用基准测试来评估当前性能,并提供优化的参考点。
  4. 利用并发,使用goroutine和通道实现并行计算和异步I/O,提高程序的响应性和利用多核处理器的能力。
  5. 减少内存分配,避免过度使用临时变量,复用对象并使用对象池来减少内存分配和垃圾回收的开销。
  6. 优化算法和数据结构,选择适当的算法和数据结构来提高性能,例如使用散列表代替线性搜索或使用排序和二分查找来加速搜索。

通过性能优化分析和实现,可以提高Go程序的效率、响应时间和可伸缩性,以提供更好的用户体验和更高的系统吞吐量。