This tutorial attempts to provide a simple and straight-forward understanding of the Windows Virtual Memory Management system, the main API functions that exist and how to use them.
Memory on modern computer systems is split up into memory pages of a fixed size. This is a collaboration between hardware and software. Sitting in your processor chip is a fantasic piece of engineering genuis known as the MMU (Memory Management Unit). What this little baby does is to take a memory address and map it using special lookup tables into a physical memory address. If we take an example, say a variable declared as char foo;, on the x86/x64 processor, this exists as two pages of physical memory, and its not contiguous. On these processors, a memory page is 4096 bytes in length. So the first 4096 characters of our declaration foo will be contiguous existing in the same physical page, but the last 4 bytes will exist in another page that might be millions of bytes away from the first page in physical memory. Seen from your point of view though it appears contiguous thanks to the MMU.
Memory is allocated in pages, and thanks to the MMU, even though multiple pages will not be in the same contiguous physical region of memory, they appear to be. Now this might seem like a crazy overcomplicated way of doing things, but trust me it's a stroke of genius.
Your contiguous address space is virtual. What makes this process even more amazing is that the MMU is also responsible for paging memory to and from disc. Yes, your program may or may not be held entirely in memory (hence the term virtual). You may be executing an instruction in one page, call a routine in another page that does not exist in memory, but has been paged to disc. Effortlessly, the MMU will read it from disc into any old spare page of memory and execute the first instruction of the call. Seen from your point of view, it simply just called the routine.
Allocating Pages of Memory
To allocate a page of memory, we call the Windows API call VirtualAlloc. This takes four parameters, the start address to be allocated (which for the moment we will say is always NULL), the amount of memory that we require in bytes, whether we want the memory to be committed or reserved (and we will ignore reserved for the moment), and finally the access flags for the memory being allocated. The return value from the call is the address of the first page allocated.
Now, the amount of memory we request is in bytes, but VirtualAlloc rounds the value up to a whole number of 4096-byte pages. So if we requested 4100 bytes for foo, VirtualAlloc would provide 8192 (2x4096 byte pages). Taking that as our first example we would say:
char *foo = (char *)VirtualAlloc(NULL, 4100, MEM_COMMIT, PAGE_READWRITE);
This says, we want 4100 bytes of committed memory and we want read and write access to said memory. More information about VirtualAlloc can be found here on MSDN.
Freeing Pages of Memory
To free pages of memory we use VirtualFree. This takes three parameters, the base address that was provided by the VirtualAlloc call, the number of bytes to free which is always NULL (for the purpose of this tutorial), and whether to decommit or release the specified memory. Again for the purpose of this tutorial, we will always use release. So, releasing the memory above would be:
VirtualFree(foo, NULL, MEM_RELEASE);
More information about VirtualFree can be found here on MSDN.
Reallocating Pages of Memory
This is somewhat problematic with the VirtualAlloc and VirtualFree functions. What we can do however, is to allocate sufficient memory of the operation, and allocate more and more pages as we need them. So, for example, let's say we have a situation where we generally will need 4096 bytes (1 pages) of memory, but in certain situations it will creep up to around 65536 bytes (16 pages). As memory is a very valuable resource, it would be poor programming if we allocated 16 pages when normally 1 pages would do, as we are depriving other applications from the memory we are rarely going to use. To acheive this, we need to issue two calls to VirtualAlloc. The first says to the operating system reserve 16 pages of my memory space. The return address is the address of the base of the memory that you have requested. It has NOT however been allocated, so any attempt to use it will cause a memory violation. What we need to do is then call VirtualAlloc again, but this time we request that the first two pages are actually commited so we can use them. The code for this would be:
char *foo = (char *)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE); VirtualAlloc(foo, 4096, MEM_COMMIT, PAGE_READWRITE);
Notice, that in the second call we have passed the address of foo (which is the base address passed by the first VirtualAlloc call). This will commit the normal one pages required. Now, to allocate subsequent pages, effectively making foo look like it's growing, we simply commit more pages from our 65536 reserved pages. So to allocate memory for address foo + 4096 to foo + 8192, we would say:
VirtualAlloc(foo+4096, 4096, MEM_COMMIT, PAGE_READWRITE);
Deallocating Pages of Memory
As with the reallocation process described above, we can deallocate memory using virtual free, by passing it the address of the page or pages we want to deallocate, the number of bytes to deallocate and the third parameter would me MEM_DECOMMIT.
If you require large amounts of dynamic memory that is relatively static once allocated, the VirtualAlloc and VirtualFree functions are extraordinarily fast and efficient. Unlike Heap functions, these functions deal with 4096 byte pages rather than individual bytes, so their useage is considerably more restricted. However, it is important not to overlook them when considering what functions to use for memory allocation - particularly in time critical applications.