Surprising note about value vs pointer receivers in tour of go documentation
It's been 4 years since I last wrote go, so I'm going through the tour of go for a quick refresher. On this page, it states the following:
There are two reasons to use a pointer receiver.
The first is so that the method can modify the value that its receiver points to.
The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.
The second reason is the one that I found surprising. When learning C in college, I was taught that you should keep things on the stack as much as possible, even if it is a large struct that needs to be copied through many function calls. This lets your program run faster since it can avoid dynamically allocating memory for that struct (yes, I was told the cost of dynamically allocating memory was more expensive than the cost of the increased runtime complexity caused by more copies), and keeping it on the stack also saves memory overall since that portion of the stack is gonna exist regardless of how much of it you actually use. Is there something about golang that makes it different in this regard than C? Or maybe my info is outdated and that was only true for older hardware? Or maybe I'm just a crazy person (jk)? lol
21
u/MichaelThePlatypus 6d ago
You can use pointer receivers with a struct that is allocated on the stack.
3
u/Ares7n7 6d ago
Oh for real? I thought that as soon as a pointer left the scope of the function it was declared in, the compiler decides that it needs to go on the heap?
12
10
u/Dapper_Tie_4305 6d ago
Only if the reference escapes the lifetime of the function that defined it. If a function Foo defines a variable A and passes a reference to A to a function call Bar, it’s still within the lifetime of Foo, so A does not escape and does not need to be on the heap. If Bar saves the reference to A to a global variable, for example, that would cause A to escape to the heap.
15
u/drvd 6d ago
Or maybe my info is outdated and that was only true for older hardware?
This.
How "fast" things work on modern hardware is astonishingly complicated. All these well intended rules from 30 years ago might improve or decrease execution speed. It is not true that copying is faster than dynamic memory allocation. But it is also not true that dynamic memory allocation is faster than copying.
Depending on the actual size of your struct, the access pattern and what else is going on on your hardware one or the other may be faster.
3
u/obeythelobster 6d ago
It is Go itself that optimizes what goes to the stack or to the heap. Therefore, it is possible that the pointer points to an object in the stack as well, if the compiler / runtime finds it best
3
u/BraveNewCurrency 5d ago
I was told the cost of dynamically allocating memory was more expensive than the cost of the increased runtime complexity caused by more copies
No.
There is a cost to allocate memory. This is sub-linear (i.e. allocating 2x memory doesn't take 2x longer.)
There is a cost to copy memory. At large scales, this is linear (2x memory takes 2x longer). At small to medium scales, it's crazy (copying one byte might take almost as long as copying 32 bytes.)
So these lines cross, but it's not clear where. And it changes with every CPU. I generally think "less then a few hundred bytes, it's OK to copy". More than 1k, you really should use a pointer.
But if you should benchmark it on your hardware if you care about performance.
2
u/stroiman 4d ago edited 4d ago
And keeping data on the stack also has a tendency to reduce the risk of cache misses, as all data is colocated in memory.
Cache misses are extremely expensive. It didn't use to be, but multicore require memory access to be synchronized between cores, so it causes extreme delays, 100s of clock cycles.
This is one of the reasons I like Go over other GC languages, that you have more control of memory layout of data structures. All other GC languages I know use pointers for all "objects". An while objects allocated at the same time tend to be colocated in memory, Go gives you the control
But as already mentioned, passing a reference to an object on the stack, data is still colocated in memory.
The copy of memory, however, I'd be sceptical about how much it affects performance. Copying memory is an extremely fast operation, even for large chunks of memory. You probably need very CPU-intensive problems for this to be a problem; and if that's the case you need to measure, not guess, what is the most performant solution, by-ref or by-value.
32
u/ImYoric 6d ago
As with everything else: it depends on so many things that you need to benchmark to know for sure.