go内存分析(pprof工具)

2,172 阅读1分钟

「这是我参与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文件,我们现在知道inputbuf的后备数组正在分配给堆。由于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.BufferRead方法替代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有一些很棒的工具,可以让我们理解编译器在进行逃逸分析时所做的决定。 基于此信息,我们可以重构代码,将不需要在堆上的值保留在栈上。永远不要将性能作为首要考虑因素来编写代码,这意味着首先要关注完整性、可读性和简单性。 一旦有了一个可用的程序,确定这个程序是否足够快。 如果它不够快,那么就使用该语言提供的工具来查找和修复性能问题。

参考翻译

www.ardanlabs.com/blog/2017/0…