memstats struct 中的哪些字段只引用堆,只引用堆栈

which fields in memstats struct refer only to heap, only to stack

Go 运行时有很多与堆和栈相关的不同变量,一些栈号是堆号的一部分,导致混淆(对我来说)。例如,in this link。它说

// Stack numbers are part of the heap numbers, separate those out for user consumption
    stats.StackSys = stats.StackInuse
    stats.HeapInuse -= stats.StackInuse
    stats.HeapSys -= stats.StackInuse

runtime docs中(下面摘录)给出了7个不同的heap相关字段(即memstat结构体的字段),没有明确说明哪些包含stack,同理,包含哪些stack字段在堆中,以及它与总分配的关系。

这是一个问题,因为我想将堆与堆栈进行比较,但我不想选择包含堆栈的堆变量(显然)。

问题 1).总分配字段是否包括堆、堆栈或两者? 2)哪些堆字段不包括数字堆栈? 3) 哪些堆字段包括堆栈的数字? 4) 哪些堆栈字段不包括堆的数字?

  Alloc      uint64 // bytes allocated and still in use
        TotalAlloc uint64 // bytes allocated (even if freed)
        Sys        uint64 // bytes obtained from system (sum of XxxSys below)
        Lookups    uint64 // number of pointer lookups
        Mallocs    uint64 // number of mallocs
        Frees      uint64 // number of frees

        // Main allocation heap statistics.
        HeapAlloc    uint64 // bytes allocated and still in use
        HeapSys      uint64 // bytes obtained from system
        HeapIdle     uint64 // bytes in idle spans
        HeapInuse    uint64 // bytes in non-idle span
        HeapReleased uint64 // bytes released to the OS
        HeapObjects  uint64 // total number of allocated objects

        // Low-level fixed-size structure allocator statistics.
        //  Inuse is bytes used now.
        //  Sys is bytes obtained from system.
        StackInuse  uint64 // bytes used by stack allocator
        StackSys    uint64

这些问题有点难回答,因为协程栈是从堆中分配的。 Go 没有 C 中存在的堆栈和堆之间的明确分离。

总分配字段是否包括堆、堆栈或两者?

MemStats 结构的 TotalAlloc 字段包括 Go 运行时从 OS 为 Go 堆请求的所有内存。它不包括为 goroutine 堆栈分配的内存。起初我认为是的,但我错了。对困惑感到抱歉。希望这个回答更准确。

(准确地说,我应该提到,在使用 cgo 的程序中,每个线程(不是 goroutine——通常 goroutine 比线程多)将有一个由 OS 分配的堆栈;那个堆栈不被Go运行时分配,不计入TotalAlloc,仅供cgo调用使用。)

哪些堆字段不包括数字堆栈? 哪些堆字段包含堆栈编号?

这些字段包括 goroutine 堆栈的数字:HeapIdle、HeapReleased。

这些字段不包括 goroutine 堆栈的数字:HeapAlloc、HeapInUse、HeapObjects。

HeapSys 字段不包括当前活动的 goroutine 堆栈使用的内存,但包括曾经使用但随后被释放的 goroutine 堆栈的内存。

哪些堆栈字段不包括堆的数字?

我不知道如何以有意义的方式回答这个问题。堆栈字段报告专门关于 goroutine 堆栈的信息。

来自 运行(变体)a test program 并查看 Go 源代码,我看到:

  • Alloc 和 TotalAlloc 似乎只涵盖非堆栈分配。分配 big locals 不会将它们的大小添加到 TotalAlloc,即使它导致堆栈增长。

  • 如果内存当前是为 goroutine 的堆栈保留的,那么它将计入 StackX 变量而不是 HeapX 变量。这是您在源代码中找到的减法。它还意味着任何为堆栈分配 space 的东西都可以减少 HeapSys 和 HeapIdle,但不影响 HeapInuse。

    • 因为您问过:堆栈字段从不包括堆分配——堆栈来自堆,反之则不然。
    • 您认为在堆上的变量 (ssp := new(SomeStruct)) 实际上可能是堆栈分配的,如果逃逸分析可以确定它们不会超过函数调用。这几乎总是对您有用,因为这些变量可以在函数退出时被释放,而不会为 GC 产生垃圾。不要太担心这个。 :)
  • 一旦 goroutine 退出,它的堆栈 space 可以 被 return 编辑到堆中。 (不过,如果它的堆栈很小,它很可能被缓存起来以作为未来 goroutine 的堆栈重用。)然后它不会显示为堆栈 space 并且可能再次显示为可用堆 space 。我在经验和 Go 源代码中都看到了这一点(proc.c 的 gfput 调用运行时·stackfree)。这意味着退出 goroutines 或旧堆栈在堆栈增长后被 returned 看起来会增长 HeapSys 和 HeapIdle,但它实际上只是 space 在使用之间转换。

  • 似乎没有 TotalAlloc-style 运行 计数器涵盖曾经为堆栈分配的所有页面。如果一个 goroutine 的堆栈被释放并重新使用,它只会被计算一次。

  • 绝对没有 TotalAlloc 风格的 运行 计数器涵盖所有堆栈分配的变量。这将涉及跟踪每个函数调用的开销。

堆栈相关的问题相对较少,因为堆栈分配的变量在函数 return 上被释放,而大堆栈本身在 goroutine 退出时被释放。它们 可以 发生,比如 goroutines 正在泄漏(即使你创建新的 goroutines 也永远不会退出),或者如果你在不退出的协程。但是堆上的问题更常见,因为堆上的东西直到 GC 才被释放(像标准 Pool 这样的工具可以帮助你 回收 堆分配延迟 GC 需求的项目)。

所以,实际上,我主要关注 Alloc 和 TotalAlloc 是否过度使用堆,如果堆栈 space 以某种方式成为问题,请查看堆栈编号(也许 check for unexpectedly many running goroutines ).

这些观察结果是特定于实现的(我正在看 go 1.4,不是 tip),而且我不是 Go 源代码的专家,所以请按原样。那个测试程序,供参考:

package main

import (
    "fmt"
    "reflect"
    "runtime"
    "sync"
)

var g []byte

func usesHeap() {
    g = make([]byte, 1000)
}

func usesTempStack() {
    var l [1000]byte
    _ = l
}

func createsGoroutineAndWaits() {
    wg := new(sync.WaitGroup)
    wg.Add(1)
    go func() {
        usesTempStack()
        wg.Done()
    }()
    wg.Wait()
}

func createsGoroutine() {
    go usesTempStack()
}

func recurse(depth int, max int) {
    var l [1024]byte
    _ = l
    if depth < max {
        recurse(depth+1, max)
    }
}

func growsTheStack() {
    recurse(0, 1000)
}

func checkUsageOf(lbl string, f func(), before, after *runtime.MemStats) {
    _ = new(sync.WaitGroup)
    runtime.ReadMemStats(before)

    // using own goroutine so everyone starts w/the same stack size
    wg := new(sync.WaitGroup)
    wg.Add(1)
    // request GC in hopes of a fair start
    runtime.GC()
    go func() {
        runtime.ReadMemStats(before)
        for i := 0; i < 1000; i++ {
            f()
        }
        runtime.Gosched()
        runtime.ReadMemStats(after)
        wg.Done()
    }()
    wg.Wait()

    fmt.Println("Results for", lbl, "\n")
    beforeVal, afterVal := reflect.ValueOf(*before), reflect.ValueOf(*after)
    memStatsType := beforeVal.Type()
    fieldCount := memStatsType.NumField()
    for i := 0; i < fieldCount; i++ {
        field := memStatsType.Field(i)
        if field.Type.Kind() != reflect.Uint64 {
            continue
        }
        beforeStat, afterStat := int64(beforeVal.Field(i).Uint()), int64(afterVal.Field(i).Uint())
        if beforeStat == afterStat {
            continue
        }
        fmt.Println(field.Name, "differs by", afterStat-beforeStat)
    }
    fmt.Println("\n")
}

func main() {
    before, after := new(runtime.MemStats), new(runtime.MemStats)
    checkUsageOf("growsTheStack", growsTheStack, before, after)
    checkUsageOf("createsGoroutineAndWaits", createsGoroutine, before, after)
    checkUsageOf("usesHeap", usesHeap, before, after)
    checkUsageOf("usesTempStack", usesTempStack, before, after)
    checkUsageOf("createsGoroutine", createsGoroutine, before, after)
}