为什么指针调用和接口调用的结果不一样?通过接口调用需要占用堆内存,速度很慢,而通过指针调用则非常快,无需占用堆内存。 以下代码基准清楚地显示了差异。当然,我希望结果是一样的。但完全不同。
#go version
go version go1.22.4 linux/amd64
我遇到了意外的内存分配,最终发现使用接口会导致意外的额外堆分配。我写了两个基准并将它们相互比较。我期待同样的结果。我不期望通过使用接口来分配额外的内存。
package main
import "testing"
type S struct{ _ [128]byte }
type Interface interface{ Play(*S) }
type Child struct{}
func (Child) Play(s *S) {}
type IParent struct{ Child Interface } //interface
type Parent struct{ Child *Child } //pointer
func (parent IParent) Work() (s S) { parent.Child.Play(&s); return }
func (parent Parent) Work() (s S) { parent.Child.Play(&s); return }
func BenchmarkInterface(b *testing.B){for range b.N{ IParent{&Child{}}.Work()}}
func BenchmarkPointer(b *testing.B){for range b.N{ Parent{&Child{}}.Work()}}
#go test -bench=. -benchmem -cpu 1
BenchmarkInterface 18390373 64.38 ns/op 128 B/op 1 allocs/op
BenchmarkPointer 1000000000 0.3562 ns/op 0 B/op 0 allocs/op
您在问题中使用的术语具有误导性,因为问题不是标题所说的。
所谓的“通过指针调用”是使用指针接收器的方法调用。来电:
Parent{&Child{}}.Work()}
可以写成
Parent.Work(Parent{&Child{}})
因此,在大多数平台中,这是对固定地址的简单 CALL 指令,并以
Parent
作为参数。
现在让我们看一下调用:
IParent{&Child{}}.Work()}
同样,这相当于:
IParent.Work(IParent{&Child{}})
IParent
是一个包含接口字段的结构体。所以IParent{&Child{}}
创建 Child
的实例,然后从中创建一个接口,并将该接口分配给 IParent.Child
。然后IParent.Work
调用Child.Play
,这会导致通过接口查找方法,因为接口的方法没有固定的地址。方法地址取决于实际实现。
然后将
&s
传递给接口方法,这会导致它转义到堆。直接调用情况并非如此,因为可以内联,并且编译器知道不需要 s
转义到堆。
除了这些之外,在编写此类微基准时还应该非常小心。他们可能不会衡量你的想法。广泛的内联和优化可能会导致运行与您编写的程序截然不同的程序。