Let's examine the differences between committed and
reserved memory in a more concrete way.
We know that memory manager uses reserved memory in order to
increase the stack size of a thread based on the actual
usage of stack for that thread,
but what other possible uses could there be for reserved memory?
So here's an example.
Suppose I want to build something which is like Microsoft Excel.
So essentially we have a big bunch of cells that the user can
use to enter data for whatever purpose.
So let's say that I would like to provide some kind of
sizes for these matrix of cells.
So let's use 2 variables here, and I'm going to use 1024,
essentially allowing the user roughly a million cells to work with.
And so I don't want to limit the user.
On the other hand,
I would like to have easy access to that memory when I
need to make changes or read data.
Let's assume again, just for demonstration purposes,
that every one of these cells could hold up to 1 KB of data.
So how can I very easily manage this memory?
One way of doing that is by using something like the malloc function to
allocate enough memory to cater for all these cells.
So I can do something like cellx * clly * cell_size,
we can allocate that memory,
and then we can very easily calculate the address of a particular cell
based on what the user needs and then perform the access.
Would that work?
Yes, that would work.
However, we're committing, malloc can only commit memory,
it knows nothing else, we're committing 1 GB of memory up front.
And it's not really clear whether the user will
actually utilize all that 1 GB of memory.
In fact, in most cases, it probably wouldn't.
So that would be fairly wasteful.
What would happen if I want to increase the size of
the matrix to even bigger sizes?
If I commit all that memory up front, I'm going to waste a lot of memory,
and at some point,
I won't even be able to do that because I'll run out of memory.
Remember that commitment requires the system to guarantee that
memory will be available to you whenever you need it.
And so it has to be guaranteed up front.
Now, here's another suggestion.
Maybe we should use reserved memory.
Instead of committing everything, let's just reserve that chunk of address space.
Of course, there's nothing there at that point,
and so reserving is going to be a very cheap operation,
and then when a user wants to access a particular cell or commit,
just that cell, or to be more precise,
the page where that cell happens to fall in,
and then make the access, so we only commit 4 KB,
that's it, for any particular cell or a bunch of adjacent cells.
So with that scheme, we're not wasting memory.
And so we're only using as much memory as we actually need based
on the actual usage of these cells by the user.
So let's see these differences when trying things out using APIs.
So malloc is not really good here; we want to use
something more powerful called VirtualAlloc,
which allows us to perform various memory allocations,
including using reserved memory.
We'll discuss some more details about this API,
the differences between that and malloc and other Windows
APIs that are provided for memory manipulation in a future
module later in this course.
So the first parameter to VirutalAlloc is the target address.
In this case,
null means I would like to get some range of addresses
that is available in the process.
I don't really care what that address should be.
I don't care.
Just find me something that is available.
And then we have the size, which we calculated in this way,
and then we need to provide the kind of memory we'd like to
actually allocate here or reserve here,
it really depends on whether we want to commit or reserve memory.
So in my first example, I'm going to commit the memory.
And technically, you have to use two flags here, MEM_COMMIT and MEM_RESERVE.
Now, technically again, you can also use just MEM_COMMIT.
However, this is not the right thing to do,
and because Microsoft had a bug in the past that used MEM_COMMIT as
a replacement for MEM_COMMIT and MEM_RESERVE,
that would work, but then because people were relying on that bug,
Microsoft can never fix it,
but the correct thing to do is to reserve and commit at the same time.
You can't really commit anything unless it was reserved first,
but you can do that at the same time by using these two flags.
I essentially want to commit this chunk of memory,
and I'm going to specify a read/write access for the returned memory.
Once I have that,
let's write something to that memory simply by using a simple string copy,
so let's just use something like "hello,
memory," or something like that,
and just use a very simple thing like strcpy and just copying into buffer.
Let me just cast that here to make it easier to work with
that memory without having to sweat a lot.
Let's just write something into the first cell,
the first cell of these matrix of cells.
So I want to copy into buffer whatever is in text,
but because that's the save function here,
I'm not going to use the old function's strcpy like that,
that's going to give me a deprecation error anyway.
And so we need to provide the maximum size we can that we need here,
and that's going to be the, well,
really size of text is is actually good enough for this purpose,
but it's not really the point.
The point is that if I commit memory, this access is bound to succeed.
There's no way in _____ this could fail.
So let's see the effect of that by placing a breakpoint and pressing
F5 just before we perform the actual operation.
Well, actually, the breakpoint was not where I intended it to be,
so here's where I want that breakpoint to be.
So before we look at that,
let's look at Task Manager and see what it has to say in terms of memory.
I'm going to go to the Performance area here and switch to Memory.
You can see what's going on in terms of memory by
looking at the counters at the bottom.
So first we see we have 6.8 GB of memory committed at this point,
and after the slash we have the commit limit,
how much more I can commit,
how much maximum memory I can commit before perhaps
page file expansion will take place,
which would allow me to commit perhaps even more memory.
Now, 6.8 is the committed size, but in RAM we only have 6.1,
which means that some of that memory is elsewhere,
not necessarily physical memory right now.
So once I perform this VirtualAlloc call with MEM_COMMIT,
what do I expect to happen?
What I expect to happen is to have the memory,
the committed size, jump by 1 GB immediately.
Even though I haven't touched that memory just yet,
this is going to jump by 1 GB,
but the in‑use memory shouldn't really budge at all because I
don't need that memory just yet in terms of RAM.
I just need the system to provide that commitment to me.
So here goes, F10,
and we can see that the committed memory has jumped to 7.8 immediately.
So now if I want to perform the strcpy here, that's not going to be a problem.
In fact,
let me add something to Visual Studio here to make
it easier to watch this memory.
I'm going to go to Debug, Windows, and there are memory windows here we can use.
And I'm going to use the buffer address here to show the memory,
and counting the memories all zeroed out.
And by the way, this is always the case with VirtualAlloc.
When you commit memory, the memory is always zeroed out, this is guaranteed.
There's no need to zero it out.
If you think about doing that, please don't.
It's just a waste of time.
If I go ahead now and use strcpy, we can see the memory appearing here,
we can see it changed as we expected, and everything is great.
If I go ahead and end this application, the memory will be released, of course.
And now if I go back to Task Manager,
we can see we're back in the 6.7 committed size.
So commitment is really the thing that we actually use to access memory.
Now, obviously, if we change this number,
let's say I want cellx to grow to, let's say,
10,240 cells in the X direction, and I try to do the same thing now,
now I'm actually asking for the system to commit roughly 10 GB of memory.
Let's see if that works out.
So, notice nothing happened, and buffer is returned as null,
which means I couldn't perform this operation.
That was too much,
that was too much for this particular system that only has 16 GB of
RAM and perhaps not a big enough page file to accommodate all that
memory for the 10 GB that I needed here.
Well, actually, I made a mistake here.
That's not really the problem.
The problem is different.
It should have worked.
I do have enough memory,
I just have to make sure I perform the calculation here correctly
because now this number is beyond 4 GB or 32‑bit,
which means it's going to be,
the calculation here is just going to truncate information,
so we have to change at least one of those to something like long long,
which would be a 64‑bit value, and now the calculation would be done correctly.
So let's try this again.
I'm going back to Task Manager.
We can see 6.8.
Let's perform the allocation again, and now it succeeds,
it works, we get back a proper pointer.
Notice the committed memory jumped immediately by 10 GB,
like immediately, even though I haven't actually used that memory in practice,
and notice that the page file had to be resized and increased in size,
so now the commit limit has increased.
So I'm using more memory now.
So obviously the difference here,
which is roughly 2.5 or 2.6 GB is what's left for
the other processes in the system.
Notice again,
physical memory hasn't changed at all because we're not really using
that memory in terms of physical access just yet.
If I perform now the operation of making that copy,
4 KB would be provided in RAM, but, of course,
these counters are too big to show that small change in memory.
So that's the idea.
If I continue to increase that, then at some point this will not work.
So if I do that, I want to now commit 100 GB of memory up front.
If I do that, that actually fails.
We get null inside buffer,
and if I go to my Watch window here and add the get last error
value and provide some textual description,
it says ERROR_COMMITMENT_LIMIT: The paging file is too small for this operation.
So the system tried to kind of increase the page file size,
but the page file can't be increased so much in this particular system,
so we can't really commit 100 GB of memory,
which, of course, makes sense.
And we know that we don't really want to work in this way.
Instead we'd like to change the way we allocate
memory by using reserved memory only.
So if I do that and I try to reserve 100 GB of memory, that works just fine.
Buffer is returned with something.
Notice the committed size didn't change, the RAM size didn't change.
However, if I look at buffer, here's what I get, I get absolutely nothing.
And that's because there's no memory there, it's just reserved memory.
This is an address space that is now reserved for a particular purpose,
future purpose.
Of course, I'm the one determining that purpose.
So if I go ahead now and try to write to this location,
I'm going to get an exception.
I'm going to hit an exception of access violation.
And here goes, you can see this exception saying,
hey, whatever you're trying to do, that's not going to pan out.
You have this access violation exception.
You may be familiar with this specific value,
C000, many 0s and 5, that's an access violation.
So we're trying to access memory which is just not committed.
There's no way to do that, and that's just going to cause the process to crash.
So obviously that's not going to be good enough.
So what can we do to solve that?
And to solve that,
what we need to do is to actually commit just the page
that we need in order to make the access.
So let's do that.
And because I'm using here the first page,
just for demonstration purposes, that's going to be fairly easy to do.
And the way to do that is to call VirtualAlloc again.
But this time I want to request a particular address,
in this case, the beginning of this block,
which is buffer because I want to commit the first cell.
So the size is going to be cell_size.
Of course,
it's always going to be rounded up to the next page boundary
because everything works in chunks of pages.
So I'm going to just ask for one cell, but,
of course,
the API will commit that particular page where that memory happens to fall.
Then MEM_COMMIT only.
This is the correct flag to use because the memory is already reserved,
it's just a matter of committing just the pieces
that I want to actually access now.
And then I'm going to use PAGE_READWRITE again because I
want to be able to write to this memory.
Let's run this now and see what happens.
So first, we perform the reserving of the memory,
and again,
just to prove that nothing interesting is happening here from
the perspective of any kind of wasting, we're not wasting anything.
If you look at buffer, we have absolutely nothing here, there's no memory.
This is Visual Studio's way of showing that there's no memory there.
And now when we commit the first cell,
which essentially is the first page going to be,
so it's actually the first four cells in practice because every
cell is 1 KB in size in our simple example,
we should see all these question marks turning into 0s immediately.
That's it because that page is now committed,
and as I mentioned, when we have committed memory using VirtualAlloc,
it's always zeroed out.
We'll understand why this is important in a later module.
So now we can definitely go ahead and write the memory,
and we get exactly the same result as before.
We can see here the memory is working as expected.
And with this scheme,
we can definitely go ahead and change these numbers to
even crazier numbers like 100 GB.
That's not going to be any problem here.
So it's actually 1 TB now.
So 1 TB,
I can reserve that without any problems because we have 128 TB
address space in the process and reserving is very cheap because
it's not really accessible memory.
It just provides a range of addresses that's now going
to be used for a new allocation.
And then we can allocate or commit just the pages that we need,
which is going to be very efficient.
So our use of memory is very efficient, and we're not going to waste memory,
but still provide the user with the flexibility to
work with any cell the user wishes.
The last thing I want to demonstrate is another way to cater for this
particular case where we want to commit something.
So if you call VirtualAlloc a second time,
maybe the user tries to write again to the same cell,
that's not an error.
So committing an already committed page is fine,
but still it's kind of wasteful because the call will go into the
kernel. The kernel is going to look to the _____ and say,
hey, that's already committed.
Why are you doing that again?
Okay, fine.
It succeeds.
It doesn't really do anything,
but this user to kernel transition is completely a waste of time,
and the time that it takes the VirtualAlloc API underneath
within the kernel to figure out that, in fact,
that page is already committed.
So we can go about it in a different way by leveraging exceptions,
exception handling,
something that we're not discussing in this course explicitly,
but the example is fairly simple.
And the basic idea is this, let's try to access that memory.
If it works, it means the memory is already committed, then it will just work.
However, if it doesn't, we're going to get that access violation exception,
and now we are able to fix it, in fact, by committing the page,
the appropriate memory that we need to commit,
and then tell the process of please try again.
And so the way to do that is by using a try except block.
This is something that is known as structured exception handling,
a mechanism Windows provides for handling exceptions.
This is something I discussed in one of the earlier Windows internal classes,
you might look that up on Pluralsight.
But in any case, we'll see that this is not very difficult to write.
I'm going to call the function here called FixMemory.
That's going to fix that memory.
I'm going to provide the exception code because if it's not an access violation,
I have no idea what to do with that, and then,
and we need to provide the address that failed to properly being accessed.
In this case, that's going to be buffer,
but in the general case,
it could be any one of these addresses within that big range of reserved memory.
So let's write this function,
and FixMemory has to return one of three possible
values indicating how to proceed.
But for now, let's look at the exception code and the pointer we need to use,
whatever that pointer may be.
The first thing I want to check is whether the code is not exception.
This is violation.
If it's access violation, we want to take care of that.
If it's not, then we have no idea what to do with that.
I'm going to return an exception code,
which is a value returned from the exception filter here called CONTINUE_SEARCH,
which means I don't want to deal with that,
please find some other potential handler for the exception,
and if you can't find it, then it's fine,
just crash the process because I'm only interested and only
know how to handle an access violation.
By the way, this symbol is just the normal exclamation mark and equals.
I'm just using a font called Cascadia Code that provides these nice
ligatures that changes the way certain operators are displayed,
so don't worry, the C language hasn't changed while you didn't notice.
So now that we have an access violation on our hands,
all we need to do is really use the same kind of code we've seen here.
We just need to commit that particular piece of memory with whatever
address we received here and tell the processor to try again.
And this is done by returning a different constant called
CONTINUE_EXECUTION saying I made some kind of fix,
please try this again.
So let's see if that works as we expect,
and then here perhaps we can just for fun read information
back from the the actual string here by displaying whatever
is in buffer at this point.
And we expect to see the string, of course, read back to us.
And you can put various breakpoints here to see what that looks like.
Let's put a breakpoint here in FixMemory and run this.
So the first thing we're going to do is to reserve the big chunk of memory,
and we know that at this point nothing should be here.
Indeed, nothing is here.
But now when we make the access, we're going to get the access violation.
Now, by default, Visual Studio thinks that this is a very severe exception,
and usually that's right,
and so it's going to stop even if we're handling that exception,
unless we change that checkbox here saying that please don't
break in that exception unless it's unhandled.
And now we're going to press F5 to continue execution,
and we'll end up in this FixMemory call,
this is our call.
I'm looking at the code here, which has this value.
Let me switch to hexadecimal just to see that C0005 thing.
So obviously we do have an access violation.
The pointer p has the same value as the buffer here because
we're accessing the first cell in our example.
I'm going to commit the page, and of course at that point,
it will be zeroed out immediately.
Let me do that.
Here goes.
So it's zeroed out.
And then we can continue running.
I can place a breakpoint here,
and the memory is going to be written correctly as it should.
We can read that back.
If you want to look at the console here, it's read back, everything is fine.
So the next time we access the same cell,
of course, in a more complex application,
this will simply succeed without any hitch and will not be able
or not need to handle any kind of exception.
This will simply work.
So hopefully you get a better sense of the difference
between reserved and committed memory.
With reserved memory, you create a bunch of address space range,
so really just a range of addresses you want to use for some future purpose,
and then you can commit things in small chunks as required.
Only the committing part is the expensive part,
and that's why we can allow ourselves here to reserve 1 TB of address space.
I can even increase that to 10 TB of address space.
Really not a problem because we have 128 TB to begin with,
so 10 TB is really nothing.
And so we can reserve really enormous chunks of memory,
but again, there's nothing there.
But then you can commit them in stages as required,
and the contingency of that address space,
that's the important part.
We have the address space contiguous.
In the case of an Excel‑like application,
this is very useful because it would be very easy to kind of jump into a particular cell with a very simple calculation.