Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are we Overusing Pass-by-Pointer in Go?

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?
like image 201
Kent Avatar asked Dec 08 '16 01:12

Kent


People also ask

Should you use pointers in Go?

However, in many cases, you will indeed need a pointer. The problem is, Golang being Golang, you are passing by value, meaning you are not passing the real p but only a clone that copies it's values. Changing the state of the clone will not affect the original. This is why you need a pointer here.

Is Golang pass by value or pass by reference?

Basic of Golang PointerPass by value will pass the value of the variable into the method, or we can say that the original variable 'copy' the value into another memory location and pass the newly created one into the method.

Is Go pass by copy?

Go is a programming language which passes by value, which effectively means that if you give a value as a parameter to a function, the received value within the function is actually a copy of the original.

Why is Go pass by value?

What is Pass-By-Value? In Go, when a parameter is passed to a function by value, it means the parameter is copied into another location of your memory. When accessing or modifying the variable within your function, only the copy is accessed or modified — the original value is never modified.


1 Answers

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.

like image 148
Vinay Pai Avatar answered Sep 22 '22 23:09

Vinay Pai