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

时间:2016-12-08 01:58:06

标签: pointers optimization go conventions calling-convention

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

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

请注意:我试图说出这一点,以便任何人都能理解,因此某些术语不精确。

一些低效的转码

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

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,抓住了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和pass-by-value?

应该考虑的事项:

  • 对于结构体,实际上比其他结构更有效,或者它们是否会被Go编译器优化为相同?
  • 依靠编译器来优化它是不好的做法吗?
  • 在任何地方传递指针是否更糟,创建容易出错的代码?

1 个答案:

答案 0 :(得分:4)

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

这里有一些快速数学...你的结构由三个float32对象组成,总共96位。假设您使用的是64位计算机,那么您的指针长度为64位,因此在最好的情况下,您可以自己保存一个微不足道的32位复制。

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

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

此外,因为您正在使用指向结构的指针,所以您要阻止编译器进行一些优化,否则它可能会进行。例如,Go可能会在将来实施register calling conventions,这将在此处予以阻止。

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