- We just started learning here now
how that escape analysis teaches us
when something allocates or not.
How something is shared will determine whether it allocates.
Now there's another part of allocation in Go,
and that is that if the compiler doesn't know the size
of a value at compile time,
it must immediately construct it on the heap,
because those frames that I've been drawing
for every function are sized at compile time.
Frames are not dynamic,
so if the compiler doesn't know the size of something
at compile time, it cannot place it on the stack.
Now the compiler knows the size of a lot of things
at compile time, it knows your struct types,
we already learned how to read that, your built in types,
other types that we're going to be using.
But sometimes you might have things like collections
that are based on, their size is based on a variable,
which gives the compilers no idea what the size of that is,
and we're going to have automatic allocations immediately.
That's the other place where you might have an allocation
in Go, compiler doesn't know the size of something,
automatic allocation.
But we talked about these stacks in Go
being very, very, very small.
The operating system stack's about 1MB,
and your Go stack is 2K.
What happens when you've got a Go routine
that's making lots of function calls
and eventually it runs out of stack space?
What's going to happen?
We can't just terminate the Go routine
like we do with a thread, and we won't.
Remember, Go is about integrity first,
it's about minimizing resources second.
We always want to try to reduce
and minimize the amount of resources that we need.
We're all on cloud computing today,
shared resources are important.
That 2K stack is a big part of that.
But when we run out of stack space,
what we need now is to get a new stack.
This is something for me and it's very unique
from a programming language, I'd never seen this before.
Basically, imagine that we had our stack,
we had some value there,
and imagine we were even sharing this value
as we move down the call stack.
Eventually, we run out of stack space.
What Go does is it has what it called contiguous stacks.
What it's going to do is allocate a larger stack,
25% larger than the original one,
and then, what it's got to do is copy all these frames
back over, in this case, these pointers are relative,
so they're very fast to fix.
But basically, that Go routine, during the function call,
we're going to take a little bit of this latency hit
on creating the larger stack, copying those frames over,
and readjusting any of these pointers.
We do this in Go, we take this hit,
one for integrity and two to minimize resources.
I told you nothing is free.
Integrity and minimizing resources come with a cost.
But this isn't something that's going to happen
all of the time.
2K is usually more than enough for our stacks,
because you usually don't go more
than even like 10 function calls deep, you don't.
There's other optimizations the compiler can do
to keep these frames very, very small.
This is a very good trade-off and balance.
But this can happen.
What this is telling us is,
which is, again, really unique for me,
is that values on your stack
can potentially be moving around.
This is a whole new world.
I mean, I'm going to show you an example of this
right here with a little piece of Go.
I've got a constant of size 10,
and here's my main, right, here's main,
and we're constructing a string value, there it is,
it says HELLO, and then I'm going to share this string value
down the call stack, you see that on line 14.
But what I've also done is allocated an array.
Arrays, we know the size of an array at compile time,
it's part of, built into the array.
This is a 40 byte array on the playground,
an integer is 4 bytes, I have 10 of them, 40 bytes.
This array is going to be placed on the stack frame.
Still going to keep the stack frame small.
But stackCopy's a recursive function.
It calls itself over and over and over again,
constantly sharing this string down the call stack.
Now, if I run this for 10 function calls or 10 iterations,
you see that the address on every iteration of the string
inside of that frame for main hasn't changed.
Beautiful.
Our 2K stack is large enough to run this program
without any issues.
But what if I change our 40 byte array
to be a 4,000 byte array?
Whoa.
Remember, we only started with a 2K stack.
That means on the first call to stackCopy,
we're going to get some growth.
That also means that that 25% is only going to go so far,
and we're going to see more growth as we run this program.
Watch what happens now when I run this program.
What you'll see is, honestly,
this is already a new stack location.
But look on index two.
On index two, we also have a new address, 55fa0.
We have another new stack.
In fact, if you look on index six, we've done it again.
Why?
Because this 4K array is being kept on the stack frame
because it doesn't need to escape.
Remember, escape analysis says we want to use the stack
and using the heap is an exception when there's integrity.
Here's a classic case of there is no escape.
But you can see that the stack has grown several times.
Cool, we're having the ability
to start with very small stacks,
which means we can have lots of Go routines,
and the stack grows when it needs to grow.
But think about the side effect that we have here.
Since a value can move in memory that's on the stack,
this actually creates an interesting constraint
for us in Go.
What this means is, is no stack can have a pointer
to another stack.
Imagine, we had all of these stacks all over the place,
hundreds of thousands of Go routines
with pointers to each other's stacks.
That would be total chaos if one stack had to grow.
Think about it.
If this stack had to grow,
we would have to track every pointer
that points to this stack and update it as well.
You want to talk about stop the world latency,
that would be insane.
One of the constraints or engineering choices here
in Go is, since our stacks can move,
it means that the only pointers to a stack
would be local pointers.
Only that stack memory is for the Go routine,
then the Go routine only.
Stack memory cannot be shared across Go routines.
Again, this is where the escape analysis come into the heap.
The heap basically now is used for any value
that's going to be shared across Go routine boundaries,
and any value that casting on the frame
because there's an integrity issue,
or any value where we don't know the size at compile time.
These are our three constraints around allocations in Go.
Again, it's because we want to really be able
to have hundreds of thousands of Go routines,
which means we need to be able
to have hundreds of thousands of stacks,
we couldn't do that if they were MG.
They have to be as small as possible.
We take the small hit,
always for integrity and for uses, small memory usage,
because that is our higher priority in Go.
What we care about is not
that our code is the fastest it can be,
we care about is it fast enough.