深入理解Golang内存性能剖析(memprofile)输出行为

深入理解Golang内存性能剖析(memprofile)输出行为 我不确定如何解读以下内存性能分析输出。我想我对其根本原理存在误解。

显示的输出是在查看性能分析时使用了“-base”选项得到的(inuse_space)。 //注意:在获取内存性能分析之前,会强制运行垃圾回收器。

场景

  1. 启动服务
  2. 获取初始内存性能分析
  3. 加入一个设备(创建一个goroutine)
  4. 配置设备
  5. 离开设备(退出goroutine)
  6. 获取操作后的内存性能分析

经过调查,我发现处理设备的goroutine累计分配了15KB的内存。

由于goroutine已经退出,这意味着在堆上分配的数据(在这个例程中)一定还在某个地方被引用着。否则,垃圾回收器应该已经将其清理掉了。(希望确认这种思路是否正确)。

现在,查看goroutine调用的最底层代码,我发现json.Marshal函数累计分配了14.62KB。

memory

.    14.62kB           				kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)

这意味着,在调用例程结束后,json.Marshal()函数在堆上存储的某些信息仍然被引用着。(也希望确认这一点。)

  1. 我假设json.Marshal()的底层代码不会保留对其解析数据的引用。
  2. 因此,我不得不假设kvPairExportJsonResult被我的代码的某个部分引用了。

查看代码,我并没有立即发现有什么会创建对kvPairExportJsonResult的引用。之后我决定进行一些测试。

//添加test1变量,进行相同的调用
//由于test1从未被使用,只是被打印。我预期test1在goroutine结束后不会保留在堆上。

kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)
test1, err1 := json.Marshal(kvPairExportResult)
fmt.Println(test1, err1)

//查看内存性能分析输出,测试结果确实符合我的预期。
.    14.62kB          kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)
.          .          test1, err1 := json.Marshal(kvPairExportResult)

当我调换顺序时,真正的困惑开始了。

//预期test1不会被分配到堆上,因为它之后没有被引用(只打印了一次)并且goroutine退出了。

test1, err1 := json.Marshal(kvPairExportResult)
fmt.Println(test1, err1)
kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)

//查看内存性能分析输出,很明显我没有看到预期的结果。
.    14.62kB	    test1, err1 := json.Marshal(kvPairExportResult)
.          .        fmt.Println(test1, err1)
.          .        kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)

嗯?奇怪……可能是因为编译器优化。 所以我关闭了优化,并再次用额外的变量进行了测试。

//现在内存肯定应该在第二次调用时分配....(?!?)
var test []KVPairExport
test1, err1 := json.Marshal(test)
fmt.Println(test1, err1)
kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)

.    14.62kB          var test []KVPairExport
.          .          test1, err1 := json.Marshal(test)
.          .          fmt.Println(test1, err1)
.          .          kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)

我不理解最后两个例子中显示的行为。 这甚至推翻了我的两个假设,并让我质疑我对这些机制的理解。

一个在打印后从未使用过的变量,怎么还会在某个地方被引用呢? 一个变量怎么会有“累计”分配却没有平坦分配?更奇怪的是,为什么它仍然保持着和之前完全相同的数量(14.62KB)? 为什么最后一个例子中的最后一行没有显示分配,而第一个(和第二个)例子却显示了?

我对这些结果感到有些困惑,我不确定是否可以信任它们。 我非常乐意接受任何帮助。


更多关于深入理解Golang内存性能剖析(memprofile)输出行为的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于深入理解Golang内存性能剖析(memprofile)输出行为的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


内存性能分析输出解读

从你的描述和测试来看,你对内存性能分析的理解基本正确,但可能对Go编译器的逃逸分析和内存分配行为存在一些误解。让我通过代码示例来解释这些现象。

1. 基础原理确认

你的第一个假设是正确的:如果goroutine已经退出,但分配的内存仍然显示在性能分析中,说明这些内存仍然被引用,没有被垃圾回收

// 示例1:验证内存引用
func processDevice() {
    // 这个slice在函数返回后可能仍然被引用
    data := make([]byte, 1024)
    _ = json.Marshal(data)
    // 如果data或marshal的结果被外部引用,内存不会被回收
}

2. json.Marshal的内存行为

json.Marshal确实不会保留对输入数据的引用,但它返回的[]byte可能被逃逸分析决定分配在堆上:

// 示例2:逃逸分析的影响
func testMarshal() []byte {
    data := map[string]interface{}{"key": "value"}
    
    // 情况1:结果被返回,逃逸到堆
    result, _ := json.Marshal(data)
    return result  // result逃逸到堆
    
    // 情况2:结果只在函数内使用,可能分配在栈
    result2, _ := json.Marshal(data)
    fmt.Println(result2)  // 可能分配在栈
    return nil
}

3. 性能分析输出解释

你观察到的现象可以通过以下代码示例来解释:

// 示例3:解释你的测试结果
package main

import (
    "encoding/json"
    "fmt"
    "runtime"
)

type KVPairExport struct {
    Key   string
    Value string
}

func testScenario1() {
    // 原始数据
    kvPairExportResult := []KVPairExport{
        {"key1", "value1"},
        {"key2", "value2"},
    }
    
    // 第一次调用:结果被赋值给变量
    kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)
    _ = kvPairExportJsonResult
    _ = err
    
    // 第二次调用:结果只用于打印
    test1, err1 := json.Marshal(kvPairExportResult)
    fmt.Println(test1, err1)
    
    // 强制GC并查看内存
    runtime.GC()
}

func testScenario2() {
    kvPairExportResult := []KVPairExport{
        {"key1", "value1"},
        {"key2", "value2"},
    }
    
    // 顺序调换:先打印的调用
    test1, err1 := json.Marshal(kvPairExportResult)
    fmt.Println(test1, err1)
    
    // 后赋值的调用
    kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)
    _ = kvPairExportJsonResult
    _ = err
    
    runtime.GC()
}

func testScenario3() {
    var test []KVPairExport
    
    kvPairExportResult := []KVPairExport{
        {"key1", "value1"},
        {"key2", "value2"},
    }
    
    // 空slice的marshal
    test1, err1 := json.Marshal(test)
    fmt.Println(test1, err1)
    
    // 实际数据的marshal
    kvPairExportJsonResult, err := json.Marshal(kvPairExportResult)
    _ = kvPairExportJsonResult
    _ = err
    
    runtime.GC()
}

func main() {
    // 运行不同场景
    testScenario1()
    testScenario2()
    testScenario3()
}

4. 关键点解释

  1. 累计分配 vs 平坦分配

    // 性能分析显示的是累计分配,不是当前分配
    // 即使内存已被释放,历史分配仍会显示
    func example() {
        for i := 0; i < 10; i++ {
            data := make([]byte, 1024)  // 累计分配10KB
            _ = data
        }
        // 当前可能没有分配,但累计分配10KB
    }
    
  2. 编译器优化影响

    func optimizedExample() {
        data := []KVPairExport{{"a", "b"}}
        
        // 编译器可能重用内存或优化掉分配
        r1, _ := json.Marshal(data)
        r2, _ := json.Marshal(data)
        
        // 实际可能只分配一次内存
        _ = r1
        _ = r2
    }
    
  3. 逃逸分析结果

    func escapeAnalysis() {
        x := make([]byte, 1024)
        
        // 情况1:逃逸到堆
        globalVar = json.Marshal(x)  // 逃逸
        
        // 情况2:可能不逃逸
        localVar, _ := json.Marshal(x)
        fmt.Println(localVar)  // 传递到fmt.Println可能引起逃逸
    }
    
    var globalVar []byte
    

5. 信任性能分析结果的建议

// 示例:更可靠的内存分析
package main

import (
    "encoding/json"
    "os"
    "runtime"
    "runtime/pprof"
)

func reliableProfiling() {
    // 1. 确保GC运行
    runtime.GC()
    
    // 2. 创建性能分析文件
    f, _ := os.Create("mem.prof")
    defer f.Close()
    
    // 3. 执行测试代码
    testCode()
    
    // 4. 再次GC
    runtime.GC()
    
    // 5. 写入性能分析数据
    pprof.WriteHeapProfile(f)
}

func testCode() {
    data := []KVPairExport{
        {"key1", "value1"},
        {"key2", "value2"},
    }
    
    // 明确控制变量生命周期
    {
        result, _ := json.Marshal(data)
        _ = result  // 在这个块结束时,result应该不再被引用
    }
    
    // 新的作用域
    {
        result2, _ := json.Marshal(data)
        _ = result2
    }
}

总结要点:

  1. 内存性能分析显示的是历史累计分配,不是当前内存使用
  2. 逃逸分析编译器优化会显著影响分配行为
  3. 变量作用域生命周期影响垃圾回收
  4. fmt.Println可能导致参数逃逸到堆
  5. 相同的分配大小(14.62KB)表明可能是相同数据结构的重复分配

性能分析结果是可信的,但需要结合Go的内存管理机制来理解。建议使用go tool pprof-alloc_space-inuse_space选项对比查看,同时注意编译器优化级别的影响。

回到顶部