Golang编译器优化导致无法对int实际大小进行基准测试 - 求助

Golang编译器优化导致无法对int实际大小进行基准测试 - 求助 我想确切了解创建一个新的整型变量(a := 3)时到底会分配多少内存。

我无法想出一个返回整型的函数来进行基准测试并查看其内存使用情况,因为编译器总是会内联该函数。

以下是我目前尝试的方法:

type IntField struct {
payload int
}

func getInt() IntField {
    return IntField{payload: 3}
}

func BenchmarkInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := getInt()
        b.payload = 4
    }
}

基准测试结果是: BenchmarkInt-8 2000000000 0.29 ns/op 0 B/op 0 allocs/op

结果显示仍然没有分配任何内存。请给我一些建议,如何让编译器不进行优化,从而得到一个真正分配整型的简单函数,这样我就能确切知道一个整型需要多少内存。

另外,请不要跟我讨论unsafe.Sizeof(),因为我认为Go运行时即使对于简单的整型变量也可能实际添加一些开销/额外内存。我该如何验证这一点以获取确凿证据?


更多关于Golang编译器优化导致无法对int实际大小进行基准测试 - 求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

大家有什么想法吗?

更多关于Golang编译器优化导致无法对int实际大小进行基准测试 - 求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


正如您所发现的,“这取决于具体情况”。它可能会被内联,也可能仅存在于寄存器中。

另外,不要跟我讨论 unsafe.Sizeof(),因为我认为即使对于简单的 int 变量,Golang 运行时实际上也可能会增加一些开销/额外内存。

您为什么会这样认为?Sizeof() 的文档甚至特别说明它"返回假设变量 v 的字节大小,就像通过 var v = x 声明 v 一样",而这正是您的问题所在。

你好 @calmh

我知道这可能听起来有点多疑,但我正在思考如何实现 Golang 运行时,我肯定无法仅用 8 字节来存储一个整数:我至少需要一个指向描述其数据类型和方法的指针。可能还需要另一个整数来存储有多少活动指针指向它。

你关于转换为接口的想法是什么——这样它就不会被内联而是强制分配?你好像编辑了你的回答。

这就是为什么我对 Sizeof() 持怀疑态度。感谢你的回复!

我之前的编辑答案是错误的。😊 更好的方法是获取指针并将其存储在全局变量中。这会强制分配内存:

func getInt() *int {
	return new(int)
}

var g *int

func BenchmarkInt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		g = getInt()
	}
}
$ go test -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkInt-8   	98839060	        11.2 ns/op	       8 B/op	       1 allocs/op
PASS

请注意,这是一个欺骗编译器的技巧。它并不意味着使用 int 通常会是八字节的分配。通常情况下,它会是零分配,作为栈的一部分或在寄存器中,就像你最初的尝试那样。

简而言之,你不应该担心这个问题。在编写代码时,还有很多其他事情值得你关注。int 的大小不在其中。

要解决编译器优化导致无法测量内存分配的问题,可以通过几种技术手段阻止编译器优化和内联。以下是几种有效的方法:

方法1:使用runtime.KeepAlive和禁止内联指令

//go:noinline
func getInt() IntField {
    return IntField{payload: 3}
}

func BenchmarkInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        result := getInt()
        result.payload = 4
        runtime.KeepAlive(result) // 防止编译器优化掉未使用的变量
    }
}

方法2:通过接口阻止优化

type intProvider interface {
    get() IntField
}

type concreteProvider struct{}

//go:noinline
func (c concreteProvider) get() IntField {
    return IntField{payload: 3}
}

func BenchmarkIntInterface(b *testing.B) {
    var provider intProvider = concreteProvider{}
    for i := 0; i < b.N; i++ {
        result := provider.get()
        result.payload = 4
        runtime.KeepAlive(result)
    }
}

方法3:使用sync/atomic防止优化

import "sync/atomic"

var sink int32

//go:noinline
func getIntAtomic() IntField {
    return IntField{payload: 3}
}

func BenchmarkIntAtomic(b *testing.B) {
    for i := 0; i < b.N; i++ {
        result := getIntAtomic()
        result.payload = 4
        // 使用atomic操作确保编译器不能优化掉
        atomic.StoreInt32(&sink, int32(result.payload))
    }
}

方法4:通过通道强制分配

//go:noinline
func getIntChannel() IntField {
    return IntField{payload: 3}
}

func BenchmarkIntChannel(b *testing.B) {
    ch := make(chan IntField, 1)
    for i := 0; i < b.N; i++ {
        result := getIntChannel()
        result.payload = 4
        ch <- result
        <-ch
    }
}

验证整型变量实际内存占用的完整示例

import (
    "runtime"
    "testing"
    "unsafe"
)

type IntField struct {
    payload int
}

//go:noinline
func createInt() IntField {
    return IntField{payload: 3}
}

func BenchmarkIntMemory(b *testing.B) {
    var totalAllocs uint64
    
    b.ReportAllocs()
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        // 强制在堆上分配
        result := createInt()
        result.payload = i // 使用循环变量防止优化
        
        // 获取内存统计
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        totalAllocs = m.Mallocs
        
        runtime.KeepAlive(result)
    }
    
    b.StopTimer()
    // 输出调试信息
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    b.Logf("Total allocs: %d, IntField size: %d bytes", m.Mallocs, unsafe.Sizeof(IntField{}))
}

// 对比测试:直接使用int
//go:noinline
func createRawInt() int {
    return 3
}

func BenchmarkRawIntMemory(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        result := createRawInt()
        runtime.KeepAlive(result)
    }
}

运行这些基准测试应该能够显示实际的内存分配情况。//go:noinline 指令是关键,它阻止编译器内联函数调用,从而确保每次调用都会产生实际的函数调用和可能的堆分配。runtime.KeepAlive() 确保变量不会被过早回收,b.ReportAllocs() 启用内存分配统计。

回到顶部