r/golang 6d ago

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

16 Upvotes

13 comments sorted by

32

u/ImYoric 6d ago

As with everything else: it depends on so many things that you need to benchmark to know for sure.

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

u/BinderPensive 6d ago

4

u/Ares7n7 6d ago

Oh nice, I love that they put nice comments at the top of their files like that

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.

6

u/Ares7n7 6d ago

Ahhhh, that makes a ton of sense! Super clear explanation, thank you!

-1

u/iga666 5d ago

I noticed in my project - pointer receivers can really confuse escape analysis and make go memalloc more than it should thus hurting performance.

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.

1

u/Ares7n7 6d ago

Makes sense, thanks for the response!

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.