Are we Overusing Pass-by-Pointer in Go?
Asked Answered
P

1

21

This question is specific to function calls, and is directed towards the trustworthiness of the Go optimizer when passing structs by value vs by pointer.

If you're wondering when to use values vs pointers in struct fields, see: Go - Performance - What's the difference between pointer and value in struct?

Please note: I've tried to word this so that it's easy for anyone to understand, some of the terminology is imprecise as a result.

Some Inefficient Go Code

Let's assume that we have a struct:

type Vec3 struct{
  X, Y, X float32
}

And we want to create a function that computes the cross product of two vectors. (For this question, the math isn't important.) There are several ways to go about this. A naive implementation would be:

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,
  }
}

Which would be called via:

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

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

This works fine, but in Go, it's apparently not very efficient. a and b are passed by value (copied) into the function, and the results are copied out again. Though this is a small example, the issues are more obvious if we consider large structs.

A more efficient implementation would be:

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)

This is harder to read and takes more space, but is more efficient. If the passed struct was very large, it would be a reasonable tradeoff.

For most people with a C-like programming background, it's intuitive to pass by reference, as much as possible, purely for efficiency.

In Go, it's intuitive to think that this is the best approach, but Go itself points out a flaw in this reasoning.

Go Is Smarter Than This

Here's something that works in Go, but cannot work in most low-level C-like languages:

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

We allocated a Vec3, grabbed a pointer the X field, and returned it to the caller. See the problem? In C, when the function returns, the stack will unwind, and the returned pointer would become invalid.

However, Go is garbage collected. It will detect that this float32 must continue to exist, and will allocate it (either the float32 or the entire Vec3) onto the heap instead of the stack.

Go requires escape detection in order for this to work. It blurs the line between pass-by-value and pass-by-pointer.

It's well known that Go is designed for aggressive optimization. If it's more efficient to pass by reference, and the passed struct is not altered by the function, why shouldn't Go take the more efficient approach?

Thus, our efficient example could be rewritten as:

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)

Notice that this is more readable, and if we assume an aggressive pass-by-value to pass-by-pointer compiler, just as efficient as before.

According to the docs, it's recommended to pass sufficiently large receivers using pointers, and to consider receivers in the same way as arguments: https://golang.org/doc/faq#methods_on_values_or_pointers

Go does escape detection on every variable already, to determine if it is placed on the heap or the stack. So it seems more within the Go paradigm to only pass by pointer if the struct will be altered by the function. This will result in more readable and less bug-prone code.

Does the Go compiler optimize pass-by-value into pass-by-pointer automatically? It seems like it should.

So Here's the Question

For structs, when should we use pass-by-pointer vs pass-by-value?

Things that should be taken into account:

  • For structs, is one actually more efficient than the other, or will they be optimized to be the same by the Go compiler?
  • Is it bad practice to rely on the compiler to optimize this?
  • Is it worse to pass-by-pointer everywhere, creating bug-prone code?
Posting answered 8/12, 2016 at 1:58 Comment(4)
The Go compiler never changes how values are passed, only values are passed and pointers are just pointer values. Use the one that makes sense, and don't worry about performance until you can prove there's a performance issue.Tomboy
It's unclear that you want to pass by reference "purely for efficiency". References are harder to analyze for aliasing and may lead to less efficient code than passing by value. It always depends.Tientiena
Someone should mention immutability, too :=) When you pass a struct "by value" to some function, you enforce the semantics that the function can't modify the original struct.Deodar
@StephaneMartin That's why I made the claim that passing by reference leads to bug-prone code. It's no longer obvious that a function will not modify a struct; and accidentally modifying a passed struct is more likely.Posting
W
16

Short answer: Yes, you're overusing pass-by-pointer here.

Some quick math here... your struct consists of three float32 objects for a total of 96 bits. Assuming you're on a 64 bit machine, your pointer is 64 bits long, so in the best case you're saving yourself a paltry 32 bits of copying.

As a price of saving those 32 bits, you're forcing an extra lookup (it needs to follow the pointer and then read the original values). It has to allocate these objects on the heap instead of the stack, which means a whole bunch of extra overhead, extra work for the garbage collector, and reduced memory locality.

When writing highly performant code, you have to be aware of the potential costs of poor memory locality cache misses can be extremely expensive. The latency of main memory can be 100x that of L1.

Furthermore, because you're taking a pointer to the struct you're preventing the compiler from making a number of optimizations it might otherwise be able to make. For example, Go might implement register calling conventions in the future, which would be prevented here.

In a nutshell, saving 4 bytes of copying could cost you quite a bit, so yes in this case you are overusing pass-by-pointer. I wouldn't use pointers just for efficiency unless the struct was 10x as large as this, and even then it's not always clear if that is the right approach given the potential for bugs caused by accidental modification.

Wivern answered 21/11, 2017 at 15:36 Comment(6)
This is, of course, an oversimplified example. I was hoping to find a "best-practice" guideline to use for selecting pass-by-reference vs pass-by-value. Go is tricky for this, because pointers & stack/heap allocation is often obfuscated by the language.Posting
That's true, it's a bit trickier in Go than say C where the difference between heap and stack allocation is much more explicit. Unfortunately there isn't a very good general answer. I tend to look at the ratio of the amount of memory being copied to the time spent in the function. If it's a very simple function, copying a structure that's a hundred bytes long is likely to dominate the cost. If it's a function that's likely to take a long time to execute (complex computation, lots of IO, etc.) you're probably better of passing by value to avoid the risks that come with passing pointers.Wivern
So apart from efficiency, what is a good reason to pass pointers? There are cases where you want a nil value, like with strings, and that would force you into using a pointer. But beyond that, what is the advantage of passing by pointer?Overwork
@Overwork the most common reason to pass by pointer is to allow the function to change the structure you're pointing to.Wivern
Yep I understand that but I've never understood why that's ever desirable if you take out performance considerations. It seems messy to change data from outside a function that's operating on it. I would assume it's a nightmare to track down bugs this way, when anything can get their hands into the data and mutate it.Overwork
@VinayPai Like your suggestion on using passing by pointer if the struct is "10x as large as this". That's the key to this question and I think go lacks this kind of guideline officially if everything is passed by value. One thing that is neglected here in the calculation is the padding needed for the structure though.Negotiable

© 2022 - 2024 — McMap. All rights reserved.