Now we are getting in some of the core concepts! Knowing this is very important to understand impact Go program will have on machine.
Everything is pass by value in Go, no matter what you pass. This also has what is you see is what you get.
Each go routine(i.e path of execution) get Stack, which is continuous memory. Go routine needs stack to do the all the allocation required. We will learn about go routine later but it is just like thread but much more lighter.
As go routine execute function it starts getting slice or portion of memory from stack that was allocated.
Lets try to understand with simple example
func main() {
counter := 0
counter++
fmt.Println("In main", counter)
inc(counter)
fmt.Println("After inc", counter)
}
Stack frame state when inc is executing
counter := 0
fmt.Println("Before pointer inc ", counter)
incByPointer(&counter)
fmt.Println("After pointer inc ", counter)
}
Go gives power of compact memory layout using Struct and efficient memory allocation using pass by value.
All the samples used in this blog is available @ pointers github repo
Everything is pass by value in Go, no matter what you pass. This also has what is you see is what you get.
Each go routine(i.e path of execution) get Stack, which is continuous memory. Go routine needs stack to do the all the allocation required. We will learn about go routine later but it is just like thread but much more lighter.
As go routine execute function it starts getting slice or portion of memory from stack that was allocated.
Lets try to understand with simple example
func main() {
counter := 0
counter++
fmt.Println("In main", counter)
inc(counter)
fmt.Println("After inc", counter)
}
Stack frame state when inc is executing
Stack Frame |
Function can only read/write to its stack frame, that is the reason why function parameters are required.
With above example any change done by inc function is local to that stack frame and if it wants to share it to caller then it has to return it, so that value can be copied to caller frame.
Another interesting properties about stack frame is that it is reusable for eg after inc function completes execution that stack frame is available to another function.
So it is like increment pointer in stack to allocate memory to function and once that function completes then decrements the counter to mark memory as free.
Pass by value is required for safety and to reason about code which is missing in many language.
Lets explore how all this changes when pointer or address of variable is passed to function.
Lets try to understand how stack frame looks when below code is executed
func main() {
counter := 0
fmt.Println("Before pointer inc ", counter)
incByPointer(&counter)
fmt.Println("After pointer inc ", counter)
}
Stack frame using pointer |
In above example parameter to function is still passed by value but this time it is of address type.
Caller knows that it has received address(&variable) of variable and to change the value , it has to use different instruction (*variable)
An asterisk (*) operator allow program to change the variable that is outside of its own stack frame, this variable can be in heap or caller function stack.
Having clear distinction when value is passed vs address of value is very power full thing as it tells that which function are doing read vs write.
Anytime you see pointer (&) , it is very clear that some mutation is happening in function.
No magical modification is possible.
Having clear distinction has couple of advantage
- Compiler can do escape analysis to determine what gets allocated to stack vs heap. This keeps GC happy because stack allocation are cheap and heap has GC overhead
- When to copy value vs share value. This is very useful thing for large values, you don't want to copy 1gb of buffer to function.
Go lang gives options to developer to choose trade off rather than giving no control.
Lets look at one more example on how allocation works
func allocateOnStack() stock {
google := stock{symbol: "GOOG", price: 1109}
return google
}
func allocateOnHeap() *stock {
google := stock{symbol: "GOOG", price: 1109}
return &google
}
Both of the above function is creating stock value but look at return type, one returns value(allocateOnStack) and other one (allocateOnHeap) returns address.
Compiler looks the return type and make a decision on what goes on stack vs heap.
So you decided what you want to throw at GC vs keep it happy.
You might have question on Stack like how big is stack ?
Each Go routine starts with 2 MB stack size, it is small and good enough to hold lots of functions call.
For most of the cases 2 MB is good but if program continues to put memory pressure on Stack then it grows to adjust the need only for specific Go routine.
Stack growth has allocation & copy cost, it is just like allocate new array and copy the value from previous array.
One nice thing about Stack memory is that it is monitored by GC and it will reduce the size of Stack if utilization of stack is around 25%.
All the samples used in this blog is available @ pointers github repo
bookmarked!!, I really like your website!
ReplyDelete