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?