「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
本文接上一篇go内存分析(基准测试和profile生成)
pprof工具
profile 文件和测试二进制文件准备就绪时,我们可以使用pprof 工具分析profile 文件了
go tool pprof -alloc_space memcpu.test mem.out
使用-alloc_space 选项替代默认的-inuse_space选项,将显示每次内存分配在哪里发生,无论在获取profile文件时是否仍在内存中。在pprof提示框下输入list algOne可以查看algOne函数。
(pprof) list algOne
Total: 335.03MB
ROUTINE ======================== .../memcpu.algOne in code/go/src/.../memcpu/stream.go
335.03MB 335.03MB (flat, cum) 100% of Total
. . 78:
. . 79:// algOne is one way to solve the problem.
. . 80:func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) {
. . 81:
. . 82: // Use a bytes Buffer to provide a stream to process.
318.53MB 318.53MB 83: input := bytes.NewBuffer(data)
. . 84:
. . 85: // The number of bytes we are looking for.
. . 86: size := len(find)
. . 87:
. . 88: // Declare the buffers we need to process the stream.
16.50MB 16.50MB 89: buf := make([]byte, size)
. . 90: end := size - 1
. . 91:
. . 92: // Read in an initial number of bytes we need to get started.
. . 93: if n, err := io.ReadFull(input, buf[:end]); err != nil || n < end {
. . 94: output.Write(buf[:n])
(pprof) _
profile文件数据和测试二进制文件就绪后,我们现在可以运行pprof工具来研究profile文件数据。根据这个profile文件,我们现在知道input和buf的后备数组正在分配给堆。由于input是一个指针变量,profile文件实际上是在说input指针所指向的bytes.Buffer值正在分配。让我们先关注input的分配并理解它为什么是分配到堆。
我们可以假设它正在分配,因为函数调用了bytes.NewBuffer共享bytes.Buffer值,它创建调用栈。 但是,pprof输出的第一列中存在一个值,这说明该值分配在堆上,因为algOne函数以某种方式共享它,从而导致它逃逸。
我们还不知道为什么这些bytes.Buffer 分配在堆上。 这就是go build命令中的-gcflags“-m -m”选项将派上用场的地方。 分析器(profiler)只能告诉您哪些值正在逃逸,但是构建命令可以告诉我们原因。
编译器信息
go build -gcflags "-m -m"
./stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer { return &bytes.Buffer literal }
./stream.go:83: &bytes.Buffer literal escapes to heap
./stream.go:83: from ~r0 (assign-pair) at ./stream.go:83
./stream.go:83: from input (assigned) at ./stream.go:83
./stream.go:83: from input (interface-converted) at ./stream.go:93
./stream.go:83: from input (passed to call[argument escapes]) at ./stream.go:93
第一行非常有趣,它确认了bytes.Buffer没有逃逸,因为它被传递到调用栈。 这是因为bytes.NewBuffer从未被调用,函数内部的代码被内联。例如,下面这段代码:
input := bytes.NewBuffer(data)
因为编译器选择内联bytes.NewBuffer函数,上述代码被转换为如下代码
input := &bytes.Buffer{buf: data}
这意味着algOne函数直接构建了byte.Buffer 。所以现在问题转变为是什么导致该值从algOne栈上逃逸,答案在另外五行。
./stream.go:83: &bytes.Buffer literal escapes to heap
./stream.go:83: from ~r0 (assign-pair) at ./stream.go:83
./stream.go:83: from input (assigned) at ./stream.go:83
./stream.go:83: from input (interface-converted) at ./stream.go:93
./stream.go:83: from input (passed to call[argument escapes]) at ./stream.go:93
input变量被分配给interface,导致了变量逃逸,io.ReadFull函数接受interface类型。所以使用interface类型作为函数参数时会导致逃逸现象发生。
我们使用bytes.Buffer的Read方法替代io.ReadFull函数,再看一下Benchmark的结果。
if _, err := input.Read(buf[end:]); err != nil {
// Flush the reset of the bytes we have.
output.Write(buf[:end])
return
}
go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out
BenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/op
减少了一次内存分配,我们看到了大约29%的性能改进。
总结
Go有一些很棒的工具,可以让我们理解编译器在进行逃逸分析时所做的决定。 基于此信息,我们可以重构代码,将不需要在堆上的值保留在栈上。永远不要将性能作为首要考虑因素来编写代码,这意味着首先要关注完整性、可读性和简单性。 一旦有了一个可用的程序,确定这个程序是否足够快。 如果它不够快,那么就使用该语言提供的工具来查找和修复性能问题。