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语言程序进行全面、深入的性能测试和分析,为程序的优化提供有力的支持。
超级会员免费看

被折叠的 条评论
为什么被折叠?



