memstats结构中哪些字段仅与堆相关,哪些字段仅与栈相关。

huangapple go评论63阅读模式
英文:

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

问题

Go运行时有许多与堆和栈相关的不同变量,其中一些栈的数字是堆数字的一部分,这让我感到困惑。例如,在这个链接中,它说:

// 栈的数字是堆数字的一部分,将其分离出来供用户使用
stats.StackSys = stats.StackInuse
stats.HeapInuse -= stats.StackInuse
stats.HeapSys -= stats.StackInuse

在运行时文档中(下面摘录),它提供了7个不同的与堆相关的字段(即memstat结构的字段),但没有清楚地解释哪些字段包括栈,以及哪些栈字段包括在堆中,以及它们与总分配的关系。

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

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

Alloc uint64 // 分配的字节数,仍在使用中
TotalAlloc uint64 // 分配的字节数(即使已释放)
Sys uint64 // 从系统获取的字节数(下面的XxxSys之和)
Lookups uint64 // 指针查找次数
Mallocs uint64 // malloc次数
Frees uint64 // free次数

// 主要分配堆统计信息。
HeapAlloc uint64 // 分配的字节数,仍在使用中
HeapSys uint64 // 从系统获取的字节数
HeapIdle uint64 // 空闲span中的字节数
HeapInuse uint64 // 非空闲span中的字节数
HeapReleased uint64 // 释放给操作系统的字节数
HeapObjects uint64 // 分配对象的总数

// 低级固定大小结构分配器统计信息。
// Inuse是当前使用的字节数。
// Sys是从系统获取的字节数。
StackInuse uint64 // 栈分配器使用的字节数
StackSys uint64

英文:

Go runtime has a lot of different variables related to heap and stack and some of the stack numbers are part of the heap numbers, leading to confusion (for me). For example, in this link. it says

// 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

In the runtime docs (excerpt below), it gives 7 different heap related fields (that is, fields of the memstat struct) without clearly explaining which ones include stack, and similarly, which stack fields are included in heap, and how that relates to total allocations.

this is a problem, because I want to compare heap against stack, but I don't want to choose a heap variable that includes stack (obviously).

Questions
1). Does total allocation field include heap, stack or both?
2) which heap fields do not include numbers stack?
3) which heap fields include numbers for stack?
4) which stack fields do not include numbers for heap?

  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

答案1

得分: 7

这些问题有点难回答,因为goroutine的堆栈是从堆中分配的。Go语言没有C语言中明确的堆栈和堆的分离。

TotalAlloc字段包括堆和堆栈吗?

MemStats结构体的TotalAlloc字段包括Go运行时从操作系统请求的所有用于Go堆的内存,但不包括为goroutine堆栈分配的内存。最初我以为它包括堆栈,但我错了。对于造成的困惑,我表示抱歉。希望这个回答更准确。

(要精确一点,我应该提到,在使用cgo的程序中,每个线程(而不是goroutine,通常goroutine的数量比线程多)都会有一个由操作系统分配的堆栈;这个堆栈不是由Go运行时分配的,也不计入TotalAlloc。它只用于cgo调用。)

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

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

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

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

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

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

英文:

These questions are a little hard to answer because the goroutine stacks are allocated from the heap. Go does not have the clear separation between stack and heap that exists in C.

Does total allocation field include heap, stack or both?

The TotalAlloc field of the MemStats struct includes all memory that the Go runtime has requested from the OS for the Go heap. It does not include memory allocated for goroutine stacks. Initially I thought it did, but I was wrong. Sorry for the confusion. I hope this answer is more accurate.

(To be precise, I should mention that in a program that uses cgo every thread (not goroutine--there are normally more goroutines than threads) will have a stack allocated by the OS; that stack is not allocated by the Go runtime and is not counted in TotalAlloc. It is only used by cgo calls.)

which heap fields do not include numbers stack?
which heap fields include numbers for stack?

These fields include numbers for goroutine stacks: HeapIdle, HeapReleased.

These fields do not include numbers for goroutine stacks: HeapAlloc, HeapInUse, HeapObjects.

The HeapSys field does not include memory used by currently active goroutine stacks, but does include memory for goroutine stacks that were once in use but were then freed.

which stack fields do not include numbers for heap?

I don't know how to answer this question in a way that makes sense. The stack fields report information specifically about goroutine stacks.

答案2

得分: 4

从运行(变体)一个测试程序并查看Go源代码的结果来看:

  • Alloc和TotalAlloc似乎只涵盖非栈分配。即使分配大的局部变量,它们的大小也不会被添加到TotalAlloc中,即使它导致堆栈增长。

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

    • 因为你问了:堆栈字段永远不包括堆分配-堆栈来自堆,但反之不成立。
    • 你认为在堆上的变量(ssp := new(SomeStruct))实际上可能是栈分配的,如果逃逸分析可以确定它们不会超出函数调用的生命周期。这对你来说几乎总是有用的,因为这些变量可以在函数退出时被释放,而不会为垃圾回收生成垃圾。不要太担心这个。 memstats结构中哪些字段仅与堆相关,哪些字段仅与栈相关。
  • 一旦goroutine退出,它的堆栈空间可以返回到堆中。(如果它的堆栈很小,它可能会被缓存以便将来的goroutine重用它作为堆栈。)然后它将不会显示为堆栈空间,而可能再次显示为可用的堆空间。我从经验上和Go源代码(proc.c的gfput调用runtime·stackfree)中看到了这一点。这意味着退出的goroutine或在堆栈增长后返回的旧堆栈可能会导致HeapSys和HeapIdle增长,但实际上只是在使用之间进行空间转移。

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

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

与堆栈相关的问题相对较少,因为堆栈分配的变量在函数返回时被释放,而大型堆栈本身在goroutine退出时被释放。它们可能会发生,比如如果goroutine泄漏(即使创建新的goroutine也不会退出),或者在不退出的goroutine中进行巨大的堆栈分配(var bigLocal [1e7]uint64,或者荒谬的深度递归)。但是与堆相关的问题更常见,因为堆上的东西直到GC才会被释放(像标准库中的Pool这样的工具可以帮助您回收堆分配的项目以延迟对GC的需求)。

因此,从实际角度来看,我主要关注Alloc和TotalAlloc以检查堆的过度使用,如果堆栈空间成为问题,可以查看堆栈数字(也许检查意外多个运行的goroutine)。

这些观察结果是特定于实现的(我正在查看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)
}
英文:

From running (variations of) a test program and peeking at the Go source, I'm seeing:

  • Alloc and TotalAlloc appear to cover only non-stack allocations. Allocating big locals did not add their size to TotalAlloc, even when it caused stacks to grow.

  • If memory is currently reserved for a goroutine's stack it counts in the StackX vars and not the HeapX vars. This is the subtraction you found in the source. It also implies anything that allocates space for stacks can reduce HeapSys and HeapIdle but leave HeapInuse alone.

    • Because you asked: stack fields never include heap allocations--stacks come from the heap but not vice versa.
    • Variables you think are on the heap (ssp := new(SomeStruct)) might in fact be stack-allocated if escape analysis can determine they don't outlive the function call. This is almost always useful to you, because those vars can be freed at function exit without generating garbage for the GC. Don't worry about this too much. memstats结构中哪些字段仅与堆相关,哪些字段仅与栈相关。
  • Once a goroutine quits, its stack space can be returned to the heap. (If its stack is small, though, it's likely be cached to be reused as a future goroutine's stack.) Then it will not show up as stack space and may show up as available heap space again. I'm seeing this both empirically and in the Go source (proc.c's gfput calling runtime·stackfree). That means that exiting goroutines or old stacks being returned after stack growth can appear to grow HeapSys and HeapIdle, but it's really just space shifting between uses.

  • There appears to be no TotalAlloc-style running counter covering all pages ever allocated for stacks. If a goroutine's stack is freed and reused it only gets counted once.

  • There is definitely no TotalAlloc-style running counter covering all stack-allocated variables ever. That would involve tracking overhead per function call.

Stack related problems are relatively rare, because stack-allocated variables are freed on function return, and large stacks themselves are freed on goroutine exit. They can happen, like if goroutines are leaking (never exiting even as you create new ones), or if you make huge stack allocations (var bigLocal [1e7]uint64, or ridiculous deep recursion) in goroutines that don't exit. But it's a lot more common to have trouble with the heap, since stuff on the heap isn't freed until GC (tools like the standard Pool help you recycle heap-allocated items to delay the need for GC).

So, practically speaking, I would mostly keep an eye on Alloc and TotalAlloc for heap overuse, and if somehow stack space becomes a problem, look at the stack numbers (and maybe check for unexpectedly many running goroutines).

These observations are implementation-specific (I'm looking at go 1.4, not tip), and I am not an expert on the Go source code, so take as what they are. That test program, for reference:

package main
import (
&quot;fmt&quot;
&quot;reflect&quot;
&quot;runtime&quot;
&quot;sync&quot;
)
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 &lt; 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 &lt; 1000; i++ {
f()
}
runtime.Gosched()
runtime.ReadMemStats(after)
wg.Done()
}()
wg.Wait()
fmt.Println(&quot;Results for&quot;, lbl, &quot;\n&quot;)
beforeVal, afterVal := reflect.ValueOf(*before), reflect.ValueOf(*after)
memStatsType := beforeVal.Type()
fieldCount := memStatsType.NumField()
for i := 0; i &lt; 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, &quot;differs by&quot;, afterStat-beforeStat)
}
fmt.Println(&quot;\n&quot;)
}
func main() {
before, after := new(runtime.MemStats), new(runtime.MemStats)
checkUsageOf(&quot;growsTheStack&quot;, growsTheStack, before, after)
checkUsageOf(&quot;createsGoroutineAndWaits&quot;, createsGoroutine, before, after)
checkUsageOf(&quot;usesHeap&quot;, usesHeap, before, after)
checkUsageOf(&quot;usesTempStack&quot;, usesTempStack, before, after)
checkUsageOf(&quot;createsGoroutine&quot;, createsGoroutine, before, after)
}

huangapple
  • 本文由 发表于 2015年1月31日 05:05:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/28244595.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定