26、Go语言性能测试与分析全解析

Go语言性能测试与分析全解析

在Go语言开发中,性能测试和分析是优化代码性能的重要手段。本文将详细介绍Go语言中性能测试的多种方法,包括运行多个性能测试用例、比较性能测试结果以及对程序进行性能剖析。

1. 运行多个性能测试用例

在Go语言中,我们可以使用子基准测试来运行多个性能测试用例。以斐波那契数列函数为例,其递归实现如下:

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

这个函数的性能取决于传入的参数 n n 越大,递归次数越多,执行时间越长。

我们可以对 fibonacci 函数进行性能测试,例如测试参数为5的情况:

func BenchmarkFibonacci5(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fibonacci(5)
    }
}

运行该测试的命令为:

% go test -run=XXX -bench=Fibonacci5

在Go 1.7之前,无法像功能测试那样进行表驱动的性能测试,因为基准测试函数只能提供一组性能结果。但从Go 1.7开始,引入了子测试功能,包括运行子基准测试,从而实现了表驱动的性能测试:

func BenchmarkFibonacciWithSubBenchmark(b *testing.B) {
    testCases := []struct {
        name string
        n    int
    }{
        {"Fibonacci-1", 1},
        {"Fibonacci-5", 5},
        {"Fibonacci-10", 10},
        {"Fibonacci-20", 20},
        {"Fibonacci-30", 30},
    }
    for _, testCase := range testCases {
        testCase := testCase
        b.Run(testCase.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                fibonacci(testCase.n)
            }
        })
    }
}

运行该测试的命令为:

% go test -run=XXX -bench=SubBenchmark

测试结果如下表所示:
| 测试用例 | 迭代次数 | 每次操作时间 |
| ---- | ---- | ---- |
| BenchmarkFibonacciWithSubBenchmark/Fibonacci-1-10 | 440615576 | 2.735 ns/op |
| BenchmarkFibonacciWithSubBenchmark/Fibonacci-5-10 | 42677919 | 27.86 ns/op |
| BenchmarkFibonacciWithSubBenchmark/Fibonacci-10-10 | 3598915 | 332.4 ns/op |
| BenchmarkFibonacciWithSubBenchmark/Fibonacci-20-10 | 29084 | 41173 ns/op |
| BenchmarkFibonacciWithSubBenchmark/Fibonacci-30-10 | 236 | 5069878 ns/op |

2. 比较性能测试结果

为了比较性能测试结果,我们可以使用 benchstat 工具。运行性能测试时,环境的一致性很重要,因为各种因素可能会影响测试结果,导致结果存在误差。因此,我们通常使用 -count 标志多次运行相同的测试。

flip 方法的基准测试为例:

func BenchmarkFlip(b *testing.B) {
    grid := load("monalisa.png")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        flip(grid)
    }
}

运行该测试10次并将结果保存到 flip.txt 文件中:

% go test -bench=BenchmarkFlip -run=XXX -count=10 > flip.txt

安装 benchstat 工具:

% go install golang.org/x/perf/cmd/benchstat

使用 benchstat 分析结果:

% benchstat flip.txt

结果示例如下:
| 名称 | 每次操作时间 |
| ---- | ---- |
| Flip-10 | 182µs ± 1% |

为了比较不同方法的性能,我们以JSON编码为例,比较 Encode Marshal 函数的性能。首先,创建一个 Person 结构体并进行JSON解码:

var jsonString string = `{"name":"Han Solo","height":"180","mass":"80",
"hair_color":"brown","skin_color":"fair","eye_color":"brown","birth_year":
"29BBY","gender":"male","homeworld":"https://swapi.dev/api/planets/22/","films":
["https://swapi.dev/api/films/1/","https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/"],"species":[],"vehicles":[],"starships":
["https://swapi.dev/api/starships/10/","https://swapi.dev/api/starships/22/"],
"created":"2014-12-10T16:49:14.582Z","edited":"2014-12-20T21:17:50.334Z",
"url":"https://swapi.dev/api/people/14/"}`
var jsonBytes []byte = []byte(jsonString)
var person Person

使用 Marshal 函数进行基准测试:

func BenchmarkWrite(b *testing.B) {
    json.Unmarshal(jsonBytes, &person)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        data, _ := json.Marshal(person)
        io.Discard.Write(data)
    }
}

运行该测试10次并保存结果到 marshal.txt 文件中:

% go test -bench=Write -run=XXX -count=10 > marshal.txt

使用 Encode 函数进行基准测试:

func BenchmarkWrite(b *testing.B) {
    json.Unmarshal(jsonBytes, &person)
    b.ResetTimer()
    encoder := json.NewEncoder(io.Discard)
    for i := 0; i < b.N; i++ {
        encoder.Encode(person)
    }
}

运行该测试10次并保存结果到 encode.txt 文件中:

% go test -bench=Write -run=XXX -count=10 > encode.txt

使用 benchstat 比较两个文件的结果:

% benchstat marshal.txt encode.txt

结果示例如下:
| 名称 | 旧方法每次操作时间 | 新方法每次操作时间 | 差异 |
| ---- | ---- | ---- | ---- |
| Write-10 | 2.16µs ± 1% | 2.10µs ± 0% | -2.49% (p=0.000 n=10+10) |

从结果可以看出, Encode 方法比 Marshal 方法快2.49%。

3. 对程序进行性能剖析

为了找出程序中哪些部分消耗了大量的CPU时间,我们可以使用 pprof 工具进行性能剖析。Go语言提供了 pprof 工具和 runtime/pprof 包来进行性能剖析。

以图像缩放函数为例,使用最近邻插值算法进行图像缩放:

func resize(grid [][]color.Color, scale float64) (resized [][]color.Color) {
    xlen, ylen := int(float64(len(grid))*scale), int(float64(len(grid[0]))*
        scale)
    resized = make([][]color.Color, xlen)
    for i := 0; i < len(resized); i++ {
        resized[i] = make([]color.Color, ylen)
    }
    for x := 0; x < xlen; x++ {
        for y := 0; y < ylen; y++ {
            xp := int(math.Floor(float64(x) / scale))
            yp := int(math.Floor(float64(y) / scale))
            resized[x][y] = grid[xp][yp]
        }
    }
    return
}

同时,需要实现图像加载和保存函数:

// load the image from file
func load(filePath string) (grid [][]color.Color) {
    file, err := os.Open(filePath)
    if err != nil {
        log.Println("Cannot read file:", err)
    }
    defer file.Close()
    img, _, err := image.Decode(file)
    if err != nil {
        log.Println("Cannot decode file:", err)
    }
    size := img.Bounds().Size()
    for i := 0; i < size.X; i++ {
        var y []color.Color
        for j := 0; j < size.Y; j++ {
            y = append(y, img.At(i, j))
        }
        grid = append(grid, y)
    }
    return
}

// save the image to file
func save(filePath string, grid [][]color.Color) {
    xlen, ylen := len(grid), len(grid[0])
    rect := image.Rect(0, 0, xlen, ylen)
    img := image.NewNRGBA(rect)
    for x := 0; x < xlen; x++ {
        for y := 0; y < ylen; y++ {
            img.Set(x, y, grid[x][y])
        }
    }
    file, err := os.Create(filePath)
    if err != nil {
        log.Println("Cannot create file:", err)
    }
    defer file.Close()
    png.Encode(file, img.SubImage(img.Rect))
}

有两种方法可以对这段代码进行性能剖析:

方法一:使用 go test 工具和 -cpuprofile 标志
创建基准测试函数:

func BenchmarkResize(b *testing.B) {
    for i := 0; i < b.N; i++ {
        grid := load("monalisa.png")
        resized := resize(grid, 3.0)
        save("resized.png", resized)
    }
}

运行测试并生成CPU剖析文件:

% go test -cpuprofile cpu.prof -bench=Resize -run=XXX

运行后会生成一个 cpu.prof 文件。

方法二:在程序代码中直接添加剖析代码
使用 github.com/pkg/profile 包简化剖析代码:

func main() {
    defer profile.Start(profile.ProfilePath(".")).Stop()
    grid := load("monalisa.png")
    resized := resize(grid, 3.0)
    save("resized.png", resized)
}

编译并运行程序:

% go build -o resize
% ./resize

运行后会生成一个 cpu.pprof 文件。

使用 pprof 工具分析剖析文件,首先需要安装 Graphviz
- macOS:

% brew install graphviz
  • Windows:从Graphviz官网下载并安装。
  • Linux(以Debian为例):
% sudo apt install graphviz

启动 pprof 的Web界面进行可视化分析:

% go tool pprof -http localhost:8080 cpu.prof

在Web界面中,我们可以查看不同的视图,如默认的图形视图、火焰图视图和源代码视图,从而找出程序中消耗大量CPU时间的部分。

通过以上方法,我们可以全面地对Go语言程序进行性能测试和分析,从而优化程序的性能。

graph LR
    A[开始] --> B[运行多个性能测试用例]
    B --> C[比较性能测试结果]
    C --> D[对程序进行性能剖析]
    D --> E[结束]
graph LR
    A[性能剖析方法] --> B[使用go test和-cpuprofile标志]
    A --> C[在程序代码中添加剖析代码]
    B --> D[生成cpu.prof文件]
    C --> E[生成cpu.pprof文件]
    D --> F[使用pprof分析]
    E --> F
    F --> G[可视化分析]

Go语言性能测试与分析全解析(续)

4. 不同性能剖析方法的结果差异与分析

我们已经通过两种不同的方法生成了CPU剖析文件,一种是使用 go test 工具和 -cpuprofile 标志,另一种是在程序代码中直接添加剖析代码。虽然我们使用相同的方法( pprof 工具)来分析这两个剖析文件,但会发现它们的结果有所不同。

使用 go test 生成的剖析文件 cpu.prof 包含了基准测试函数的相关信息,因为它是在基准测试的环境下运行的。而通过在程序代码中添加剖析代码生成的 cpu.pprof 文件,由于是在程序正常运行时生成的,不包含基准测试的部分,所以剖析结果中基准测试相关的信息会缺失。

不过,这两种方法对于代码中各个函数的CPU时间占比分析结果应该大致相同。例如,在图像缩放的例子中,无论是哪种方法生成的剖析文件, save 函数在编码图像时消耗的CPU时间占比、 load 函数在解码图像时消耗的CPU时间占比等关键信息应该是一致的。

5. pprof工具的不同视图分析

pprof 工具提供了多种视图来帮助我们分析程序的性能,下面详细介绍几种常见视图及其用途。

图形视图(Graph View)
这是 pprof 的默认视图。在图形视图中,我们可以看到各个函数之间的调用关系,以及每个函数消耗的CPU时间占比。颜色越深,表示该函数消耗的CPU时间越多。例如,在图像缩放程序的剖析结果中,我们可能会看到 save 函数的节点颜色较深,这说明它在整个程序运行过程中消耗了较多的CPU时间。

火焰图视图(Flame Graph View)
火焰图是一种非常直观的性能分析工具。在火焰图中,每个矩形代表一个函数调用,矩形的宽度表示该函数调用消耗的CPU时间。从下往上看,代表函数调用的层级关系。点击水平矩形条,可以展开查看更详细的调用信息。在图像缩放程序的火焰图中,如果我们看到 BenchmarkResize 函数出现多次,说明基准测试函数进行了多次迭代。当我们将基准测试函数的迭代次数设置为1时,火焰图中就只会出现一次 BenchmarkResize 函数。

源代码视图(Source View)
源代码视图显示了代码中各个重要行的扁平时间(flat time,即该函数自身执行所消耗的时间)和累积时间(cumulative time,即该函数及其子函数执行所消耗的总时间)。由于代码量可能较大,我们可以通过搜索功能,快速定位到我们感兴趣的函数。例如,在图像缩放程序中,我们可以搜索 resize 函数,查看该函数中每一行代码的CPU时间消耗情况。

6. 性能测试与分析的最佳实践

为了确保性能测试和分析的结果准确可靠,我们需要遵循一些最佳实践。

环境一致性
在进行性能测试时,要尽量保证测试环境的一致性。避免在测试过程中进行其他可能占用网络、内存和CPU资源的操作,如浏览网页、读取邮件等。同时,要注意系统的热缩放、垃圾回收、系统更新等背景进程可能对测试结果产生的影响。

多次测试
使用 -count 标志多次运行相同的测试,以减少测试结果的误差。一般来说,运行次数越多,结果的可靠性越高。例如,在测试 flip 方法时,我们运行了10次测试,并使用 benchstat 工具分析结果,这样可以得到更准确的性能指标。

合理选择测试用例
在进行性能测试时,要根据实际情况选择合适的测试用例。例如,在测试斐波那契数列函数时,我们选择了不同的参数值(如1、5、10、20、30)进行测试,这样可以更全面地了解函数在不同输入下的性能表现。

及时分析结果
得到性能测试和分析结果后,要及时进行分析。如果发现某个函数消耗的CPU时间过长,或者某个操作的性能指标不理想,要深入分析原因,并尝试进行优化。例如,如果发现 save 函数在图像编码时消耗的CPU时间过多,可以考虑优化编码算法或者使用更高效的库。

7. 总结

通过本文的介绍,我们了解了Go语言中性能测试和分析的多种方法。可以使用子基准测试运行多个性能测试用例,使用 benchstat 工具比较不同性能测试结果,使用 pprof 工具对程序进行性能剖析。同时,我们还学习了 pprof 工具的不同视图及其用途,以及性能测试和分析的最佳实践。

在实际开发中,我们应该根据具体需求选择合适的性能测试和分析方法,及时发现并解决程序中的性能问题,从而提高程序的运行效率和稳定性。

graph LR
    A[性能测试与分析] --> B[环境一致性]
    A --> C[多次测试]
    A --> D[合理选择测试用例]
    A --> E[及时分析结果]
    B --> F[准确可靠的结果]
    C --> F
    D --> F
    E --> F
视图名称 用途
图形视图 展示函数调用关系和各函数CPU时间占比
火焰图视图 直观显示函数调用层级和各函数CPU时间消耗
源代码视图 查看代码中重要行的扁平时间和累积时间

通过以上的方法和工具,我们可以对Go语言程序进行全面、深入的性能测试和分析,为程序的优化提供有力的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值