准确测量堆增长
Measure heap growth accurately
我正在尝试测量调用函数前后堆分配对象数量的变化。我强制使用 runtime.GC() 并使用 runtime.ReadMemStats 来测量我前后拥有的堆对象的数量。
我遇到的问题是 有时 看到意外的堆增长。而且每次运行.
后都不一样
下面是一个简单的例子,我总是希望看到零堆对象增长。
https://go.dev/play/p/FBWfXQHClaG
var mem1_before, mem2_before, mem1_after, mem2_after runtime.MemStats
func measure_nothing(before, after *runtime.MemStats) {
runtime.GC()
runtime.ReadMemStats(before)
runtime.GC()
runtime.ReadMemStats(after)
}
func main() {
measure_nothing(&mem1_before, &mem1_after)
measure_nothing(&mem2_before, &mem2_after)
log.Printf("HeapObjects diff = %d", int64(mem1_after.HeapObjects-mem1_before.HeapObjects))
log.Printf("HeapAlloc diff %d", int64(mem1_after.HeapAlloc-mem1_before.HeapAlloc))
log.Printf("HeapObjects diff = %d", int64(mem2_after.HeapObjects-mem2_before.HeapObjects))
log.Printf("HeapAlloc diff %d", int64(mem2_after.HeapAlloc-mem2_before.HeapAlloc))
}
示例输出:
2009/11/10 23:00:00 HeapObjects diff = 0
2009/11/10 23:00:00 HeapAlloc diff 0
2009/11/10 23:00:00 HeapObjects diff = 4
2009/11/10 23:00:00 HeapAlloc diff 1864
我想做的事情不切实际吗?我假设 运行time 正在做 allocate/free 堆内存的事情。我可以让它停下来进行测量吗? (这是为了测试检查内存泄漏,而不是生产代码)
您无法预测后台需要进行哪些垃圾收集和读取所有内存统计信息。调用它们来计算内存分配和使用情况并不是一种可靠的方法。
幸运的是,Go 的 testing framework 可以监控和计算内存使用情况。
所以你应该做的是编写一个基准函数,让测试框架完成它的工作来报告内存分配和使用情况。
假设我们要测量这个 foo()
函数:
var x []int64
func foo(allocs, size int) {
for i := 0; i < allocs; i++ {
x = make([]int64, size)
}
}
它所做的只是分配给定 size
的一部分,并以给定的次数 (allocs
) 执行此操作。
让我们为不同的场景编写基准测试函数:
func BenchmarkFoo_0_0(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(0, 0)
}
}
func BenchmarkFoo_1_1(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(1, 1)
}
}
func BenchmarkFoo_2_2(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(2, 2)
}
}
运行 与 go test -bench . -benchmem
的基准,输出是:
BenchmarkFoo_0_0-8 1000000000 0.3204 ns/op 0 B/op 0 allocs/op
BenchmarkFoo_1_1-8 67101626 16.58 ns/op 8 B/op 1 allocs/op
BenchmarkFoo_2_2-8 27375050 42.42 ns/op 32 B/op 2 allocs/op
如您所见,每个函数调用的分配与我们作为 allocs
参数传递的相同。分配的内存是预期的 allocs * size * 8 bytes
.
请注意,报告的每个操作的分配是一个整数值(它是整数除法的结果),因此如果基准函数只是偶尔分配,则可能不会在整数结果中报告。有关详细信息,请参阅 Output from benchmem。
就像这个例子:
var x []int64
func bar() {
if rand.Float64() < 0.3 {
x = make([]int64, 10)
}
}
此 bar()
函数以 30% 的概率 进行 1 次分配(并且 none 以 70% 的概率 ),这意味着在 平均 上它进行 0.3 次分配。对其进行基准测试:
func BenchmarkBar(b *testing.B) {
for i := 0; i < b.N; i++ {
bar()
}
}
输出为:
BenchmarkBar-8 38514928 29.60 ns/op 24 B/op 0 allocs/op
我们可以看到有 24 字节分配(0.3 * 10 * 8 字节),这是正确的,但报告的每个操作的分配是 0。
幸运的是,我们还可以使用 testing.Benchmark()
function. It returns a testing.BenchmarkResult
对我们主应用程序的功能进行基准测试,包括有关内存使用情况的所有详细信息。我们可以访问分配总数和迭代次数,因此我们可以使用浮点数计算每个操作的分配:
func main() {
rand.Seed(time.Now().UnixNano())
tr := testing.Benchmark(BenchmarkBar)
fmt.Println("Allocs/op", tr.AllocsPerOp())
fmt.Println("B/op", tr.AllocedBytesPerOp())
fmt.Println("Precise allocs/op:", float64(tr.MemAllocs)/float64(tr.N))
}
这将输出:
Allocs/op 0
B/op 24
Precise allocs/op: 0.3000516369276302
我们可以看到每个操作预期的 ~0.3 分配。
现在,如果我们继续对您的 measure_nothing()
函数进行基准测试:
func BenchmarkNothing(b *testing.B) {
for i := 0; i < b.N; i++ {
measure_nothing(&mem1_before, &mem1_after)
}
}
我们得到这个输出:
Allocs/op 0
B/op 11
Precise allocs/op: 0.12182030338389732
如您所见,运行 两次垃圾收集器和两次读取内存统计信息偶尔需要分配(10 次调用中约有 1 次:平均 0.12 次)。
我正在尝试测量调用函数前后堆分配对象数量的变化。我强制使用 runtime.GC() 并使用 runtime.ReadMemStats 来测量我前后拥有的堆对象的数量。
我遇到的问题是 有时 看到意外的堆增长。而且每次运行.
后都不一样下面是一个简单的例子,我总是希望看到零堆对象增长。
https://go.dev/play/p/FBWfXQHClaG
var mem1_before, mem2_before, mem1_after, mem2_after runtime.MemStats
func measure_nothing(before, after *runtime.MemStats) {
runtime.GC()
runtime.ReadMemStats(before)
runtime.GC()
runtime.ReadMemStats(after)
}
func main() {
measure_nothing(&mem1_before, &mem1_after)
measure_nothing(&mem2_before, &mem2_after)
log.Printf("HeapObjects diff = %d", int64(mem1_after.HeapObjects-mem1_before.HeapObjects))
log.Printf("HeapAlloc diff %d", int64(mem1_after.HeapAlloc-mem1_before.HeapAlloc))
log.Printf("HeapObjects diff = %d", int64(mem2_after.HeapObjects-mem2_before.HeapObjects))
log.Printf("HeapAlloc diff %d", int64(mem2_after.HeapAlloc-mem2_before.HeapAlloc))
}
示例输出:
2009/11/10 23:00:00 HeapObjects diff = 0
2009/11/10 23:00:00 HeapAlloc diff 0
2009/11/10 23:00:00 HeapObjects diff = 4
2009/11/10 23:00:00 HeapAlloc diff 1864
我想做的事情不切实际吗?我假设 运行time 正在做 allocate/free 堆内存的事情。我可以让它停下来进行测量吗? (这是为了测试检查内存泄漏,而不是生产代码)
您无法预测后台需要进行哪些垃圾收集和读取所有内存统计信息。调用它们来计算内存分配和使用情况并不是一种可靠的方法。
幸运的是,Go 的 testing framework 可以监控和计算内存使用情况。
所以你应该做的是编写一个基准函数,让测试框架完成它的工作来报告内存分配和使用情况。
假设我们要测量这个 foo()
函数:
var x []int64
func foo(allocs, size int) {
for i := 0; i < allocs; i++ {
x = make([]int64, size)
}
}
它所做的只是分配给定 size
的一部分,并以给定的次数 (allocs
) 执行此操作。
让我们为不同的场景编写基准测试函数:
func BenchmarkFoo_0_0(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(0, 0)
}
}
func BenchmarkFoo_1_1(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(1, 1)
}
}
func BenchmarkFoo_2_2(b *testing.B) {
for i := 0; i < b.N; i++ {
foo(2, 2)
}
}
运行 与 go test -bench . -benchmem
的基准,输出是:
BenchmarkFoo_0_0-8 1000000000 0.3204 ns/op 0 B/op 0 allocs/op
BenchmarkFoo_1_1-8 67101626 16.58 ns/op 8 B/op 1 allocs/op
BenchmarkFoo_2_2-8 27375050 42.42 ns/op 32 B/op 2 allocs/op
如您所见,每个函数调用的分配与我们作为 allocs
参数传递的相同。分配的内存是预期的 allocs * size * 8 bytes
.
请注意,报告的每个操作的分配是一个整数值(它是整数除法的结果),因此如果基准函数只是偶尔分配,则可能不会在整数结果中报告。有关详细信息,请参阅 Output from benchmem。
就像这个例子:
var x []int64
func bar() {
if rand.Float64() < 0.3 {
x = make([]int64, 10)
}
}
此 bar()
函数以 30% 的概率 进行 1 次分配(并且 none 以 70% 的概率 ),这意味着在 平均 上它进行 0.3 次分配。对其进行基准测试:
func BenchmarkBar(b *testing.B) {
for i := 0; i < b.N; i++ {
bar()
}
}
输出为:
BenchmarkBar-8 38514928 29.60 ns/op 24 B/op 0 allocs/op
我们可以看到有 24 字节分配(0.3 * 10 * 8 字节),这是正确的,但报告的每个操作的分配是 0。
幸运的是,我们还可以使用 testing.Benchmark()
function. It returns a testing.BenchmarkResult
对我们主应用程序的功能进行基准测试,包括有关内存使用情况的所有详细信息。我们可以访问分配总数和迭代次数,因此我们可以使用浮点数计算每个操作的分配:
func main() {
rand.Seed(time.Now().UnixNano())
tr := testing.Benchmark(BenchmarkBar)
fmt.Println("Allocs/op", tr.AllocsPerOp())
fmt.Println("B/op", tr.AllocedBytesPerOp())
fmt.Println("Precise allocs/op:", float64(tr.MemAllocs)/float64(tr.N))
}
这将输出:
Allocs/op 0
B/op 24
Precise allocs/op: 0.3000516369276302
我们可以看到每个操作预期的 ~0.3 分配。
现在,如果我们继续对您的 measure_nothing()
函数进行基准测试:
func BenchmarkNothing(b *testing.B) {
for i := 0; i < b.N; i++ {
measure_nothing(&mem1_before, &mem1_after)
}
}
我们得到这个输出:
Allocs/op 0
B/op 11
Precise allocs/op: 0.12182030338389732
如您所见,运行 两次垃圾收集器和两次读取内存统计信息偶尔需要分配(10 次调用中约有 1 次:平均 0.12 次)。