指针与接口

问题描述 投票:0回答:1

为什么指针调用和接口调用的结果不一样?通过接口调用需要占用堆内存,速度很慢,而通过指针调用则非常快,无需占用堆内存。 以下代码基准清楚地显示了差异。当然,我希望结果是一样的。但完全不同。

#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
go pointers memory-management interface heap
1个回答
0
投票

您在问题中使用的术语具有误导性,因为问题不是标题所说的。

所谓的“通过指针调用”是使用指针接收器的方法调用。来电:

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
转义到堆。

除了这些之外,在编写此类微基准时还应该非常小心。他们可能不会衡量你的想法。广泛的内联和优化可能会导致运行与您编写的程序截然不同的程序。

© www.soinside.com 2019 - 2024. All rights reserved.