首页 文章

我们是否在Go中过度使用指针传递?

提问于
浏览
5

这个问题特定于函数调用,并且当通过值与指针传递结构时,指向Go优化器的可信度 .

如果您想知道何时在struct字段中使用值vs指针,请参阅:Go - Performance - What's the difference between pointer and value in struct?

Please note: 我很容易理解,因此某些术语是不精确的 .

一些低效的Go Code

我们假设我们有一个结构:

type Vec3 struct{
  X, Y, X float32
}

我们想要创建一个计算两个向量的叉积的函数 . (对于这个问题,数学并不重要 . )有几种方法可以解决这个问题 . 一个天真的实现将是:

func CrossOf(a, b Vec3) Vec3{
  return Vec3{
    a.Y*b.Z - a.Z*b.Y,
    a.Z*b.X - a.X*b.Z,
    a.X*b.Y - a.Y*b.X,
  }
}

通过以下方式调用:

a:=Vec3{1,2,3}
b:=Vec3{2,3,4}
var c Vec3

// ...and later on:
c := CrossOf(a, b)

这很好用,但在Go中,它显然效率不高 . ab 按值传递(复制)到函数中,结果将再次复制出来 . 虽然这是一个小例子,但如果我们考虑大结构,问题就更明显了 .

更有效的实施方案是:

func (res *Vec3) CrossOf(a, b *Vec3) {
  // Cannot assign directly since we are using pointers.  It's possible that a or b == res
  x := a.Y*b.Z - a.Z*b.Y
  y := a.Z*b.X - a.X*b.Z
  res.Z = a.X*b.Y - a.Y*b.X 
  res.Y = y
  res.X = x
}

// usage
c.CrossOf(&a, &b)

这更难以阅读并占用更多空间,但效率更高 . 如果传递的结构非常大,那将是一个合理的权衡 .

对于大多数具有C语言编程背景的人来说,尽可能通过引用直接传递,纯粹是为了提高效率 .

在Go中,直觉上认为这是最好的方法,但Go本身指出了这种推理的缺陷 .

Go比这更聪明

这里有一些适用于Go的东西,但不适用于大多数低级C语言:

func GetXAsPointer(vec Vec3) *float32{
  return &vec.X
}

我们分配了 Vec3 , grab 了 X 字段的指针,并将其返回给调用者 . 看到问题?在C中,当函数返回时,堆栈将展开,返回的指针将变为无效 .

然而,Go是垃圾收集 . 它将检测到 float32 必须继续存在,并将它( float32 或整个 Vec3 )分配到堆而不是堆栈 .

Go需要转义检测才能使其正常工作 . 它模糊了pass-by-value和pass-by-pointer之间的界限 .

众所周知,Go专为积极优化而设计 . 如果通过引用传递更有效,并且函数不改变传递的结构,为什么Go不应采用更有效的方法?

因此,我们的有效示例可以重写为:

func (res *Vec3) CrossOf(a, b Vec3) {
    res.X = a.Y*b.Z - a.Z*b.Y
    rex.Y = a.Z*b.X - a.X*b.Z
    res.Z = a.X*b.Y - a.Y*b.X
}

// usage
c.CrossOf(a, b)

请注意,这更具可读性,并且如果我们假设对指针传递编译器进行积极的传值,就像以前一样有效 .

根据文档,建议使用指针传递足够大的接收器,并以与参数相同的方式考虑接收器:https://golang.org/doc/faq#methods_on_values_or_pointers

Go确实已经对每个变量进行了检测,以确定它是放在堆还是堆栈上 . 因此,如果结构将被函数更改,那么在Go范例中似乎只能通过指针传递 . 这将导致更具可读性和更少错误的代码 .

Go编译器是否自动将pass-by-value优化为pass-by-pointer?好像应该这样 .

所以这是问题

对于结构体,我们何时应该使用pass-by-pointer vs pass-by-value?

应该考虑的事情:

  • 对于结构体,实际上是一个比另一个更有效,或者它们是否会被Go编译器优化为相同?

  • 依靠编译器来优化它是不好的做法?

  • 在任何地方传递指针是否更糟,创建容易出错的代码?

1 回答

  • 1

    简短的回答:是的,你在这里过度使用传递指针 .

    这里有一些快速数学...你的结构由三个float32对象组成,总共96位 . 假设你是64位机器,你的指针是64位长,所以在最好的情况下,你可以节省一些微不足道的32位复制 .

    作为保存这32位的代价,你需要额外的查找(它需要跟随指针然后读取原始值) . 它必须在堆而不是堆栈上分配这些对象,这意味着一大堆额外的开销,垃圾收集器的额外工作以及减少的内存局部性 .

    在编写高性能代码时,您必须意识到可怜的内存局部性缓存未命中的潜在成本可能非常昂贵 . 主存储器的延迟可以是L1的100倍 .

    此外,因为您在某种程度上阻止编译器进行一些优化,否则它可能会进行优化 . 例如,Go可能会实现register calling conventions在将来,这将被阻止 .

    简而言之,保存4个字节的复制可能会花费你很多,所以是的,在这种情况下你过度使用pass-by-pointer . 我不会仅仅为了效率而使用指针,除非结构的大小是这个的10倍,即使这样,如果这是正确的方法,并不总是很清楚,因为意外修改可能导致错误 .

相关问题