10

I would like to resize a region of memory allocated by MS window's VirtualAlloc. Looking at the VirtualFree documentation, it is possible to decommit a region only partly, but it's not possible to partially release it. That is, it's possible to release part of the physical memory, but not part of the virtual memory.

I'm aware it may be necessary to reallocate the region in such a case. However, copying over the entire region would be rather inefficient. Is there a way to ask windows to allocate a new region with a different size, that points to the same memory?

cib
  • 2,124
  • 2
  • 21
  • 23
  • The whole point of virtual memory is to abstract away this stuff and let the kernel deal with memory placement. – Jonathan Grynspan Sep 21 '11 at 18:24
  • Does your application need to release pages of virtual memory; are you running out of virtual address space? – Daniel Trebbien Sep 21 '11 at 18:32
  • 1
    @DanielTrebbien: The situation is as follows.. I need a new region, so I try to allocate it with VirtualAlloc. However, VirtualAlloc throws an out-of-memory. At the same time, because of some metadata I'm keeping myself, I know that certain regions are only half-full. I'd now like to shrink or realloc those regions cheaply until I have memory again. – cib Sep 21 '11 at 18:44
  • 2
    You can't make this work. Moving allocations is only possible with a garbage collector that can track pointers and update them. Decommitting regions don't shrink the allocation, it merely unmaps them. You are finding out why malloc() works the way it does. – Hans Passant Sep 21 '11 at 19:17
  • There's no unchecked pointers referencing the pages. It's fine if the VMem addresses are changed. i.e. getting something like realloc would be good enough, as long as it's fast. – cib Sep 21 '11 at 19:55

3 Answers3

5

As you have mentioned, it does not appear to be possible to partially release a range of reserved pages because the VirtualFree() documentation states:

If the dwFreeType parameter is MEM_RELEASE, [lpAddress] must be the base address returned by the VirtualAlloc function when the region of pages [was] reserved.

as well as:

If the dwFreeType parameter is MEM_RELEASE, [dwSize] must be 0 (zero).

VirtualFree() is itself a thin wrapper of the kernel function NtFreeVirtualMemory(). Its documentation page (the same as for ZwFreeVirtualMemory()) also has this wording.

One possible work-around is to split up a single, large reservation with multiple smaller ones. For example, suppose that you normally reserve 8 MiB of virtual address space at a time. You could instead attempt to reserve the range in thirty-two contiguous 256 KiB reservations. The first 256 KiB reservation would contain a 32-bit unsigned bit field, where the ith bit is set if the ith 256 KiB reservation was obtained:

#define NOMINMAX
#include <windows.h>
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#define RESERVATION_SIZE (256*1024)

typedef struct st_first_reservation {
    size_t reservation_size;
    uint32_t rfield;
    char premaining[0];
} st_first_reservation;

int main()
{
    SYSTEM_INFO sys_info = { 0 };
    GetSystemInfo(&sys_info);

    assert((RESERVATION_SIZE % sys_info.dwPageSize) == 0);

    void *vp = VirtualAlloc(NULL, 32*RESERVATION_SIZE, MEM_RESERVE, PAGE_NOACCESS);
    if (VirtualFree(vp, 0, MEM_RELEASE) == 0) {
        fprintf(stderr, "Error: VirtualFree() failed.\n");
        return EXIT_FAILURE;
    }

    st_first_reservation *pfirst_reservation = (st_first_reservation *) VirtualAlloc(vp, RESERVATION_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (pfirst_reservation == NULL) {
        pfirst_reservation = (st_first_reservation *) VirtualAlloc(NULL, RESERVATION_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
        if (pfirst_reservation == NULL) {
            fprintf(stderr, "Error: VirtualAlloc() failed.\n");
            return EXIT_FAILURE;
        }
    }

    fprintf(stderr, "pfirst_reservation = 0x%p\n", (void *) pfirst_reservation);

    pfirst_reservation->reservation_size = RESERVATION_SIZE;
    pfirst_reservation->rfield = 1LU;

    char *p = (char *) pfirst_reservation;
    unsigned i = 1;
    for (; i < 32; ++i) {
        vp = VirtualAlloc(p += RESERVATION_SIZE, RESERVATION_SIZE, MEM_RESERVE, PAGE_NOACCESS);
        if (vp != NULL) {
            assert(((void *) vp) == p);
            pfirst_reservation->rfield |= 1LU << i;
            fprintf(stderr, "Obtained reservation #%u\n", i + 1);
        } else {
            fprintf(stderr, "Failed to obtain reservation #%u\n", i + 1);
        }
    }

    fprintf(stderr, "pfirst_reservation->rfield = 0x%08x\n", pfirst_reservation->rfield);

    return EXIT_SUCCESS;
}

Sample output:

pfirst_reservation = 0x009A0000
Obtained reservation #2
Obtained reservation #3
Obtained reservation #4
Obtained reservation #5
Obtained reservation #6
Obtained reservation #7
Obtained reservation #8
Obtained reservation #9
Obtained reservation #10
Obtained reservation #11
Obtained reservation #12
Obtained reservation #13
Obtained reservation #14
Obtained reservation #15
Obtained reservation #16
Obtained reservation #17
Obtained reservation #18
Obtained reservation #19
Obtained reservation #20
Obtained reservation #21
Obtained reservation #22
Obtained reservation #23
Obtained reservation #24
Obtained reservation #25
Obtained reservation #26
Obtained reservation #27
Obtained reservation #28
Obtained reservation #29
Obtained reservation #30
Obtained reservation #31
Obtained reservation #32
pfirst_reservation->rfield = 0xffffffff

EDIT: I have found that it is much better to "pre-reserve" the thirty-two 256 KiB ranges all at once, free, and then try to re-reserve as many as you can.

I updated the code and sample output above.

In a multithreaded environment, the code may fall back to the "place anywhere" allocation of the first reservation. Perhaps it is a good idea to attempt reserving RESERVATION_SIZE bytes at a reserved-then-freed range of 32*RESERVATION_SIZE bytes five or so times, finally falling back to the "place anywhere" allocation.

Daniel Trebbien
  • 38,421
  • 18
  • 121
  • 193
  • Interesting, I had that same idea, but I wasn't sure if it would be efficient or how it would be done in detail. In fact, if I think about it, a bitmap isn't even necessary, since I will only be shrinking or growing the "region". Thus, a counter of currently allocated pages should suffice. Anyway, thanks a lot for the code, too bad I can't vote up more than once. – cib Sep 21 '11 at 21:05
  • @cib: I am not sure whether you can get by without the bitmap. It can happen, for example, that the 32nd reservation could not be obtained. Even if you protect all calls to `VirtualAlloc()` with a mutex, it is possible for another process to allocate virtual address space in your process' address space via [`VirtualAllocEx()`](http://msdn.microsoft.com/en-us/library/aa366890.aspx). – Daniel Trebbien Sep 21 '11 at 21:19
  • I think that once I am unable to provide a contiguous chunk of memory, I have to either reallocate or raise an error. If my "region" has holes in it, I can not use it as a single chunk of memory, but rather have to treat it as what it is, a collection of separate pages. Also, while this is a bit off-topic, can you explain why other processes may want to allocate memory in my process? It sounds like something that won't be safe anyway, without a proper inter-process permission system. – cib Sep 21 '11 at 21:41
  • @cib: I have only used `VirtualAllocEx()` once to allocate some memory within `explorer.exe`. IIRC, I needed to allocate space for a specific structure within Explorer's address space so that I could send a message to one of Explorer's windows (corresponding to the "notification area" of the start bar). This window would fill out the structure with information. I then called `ReadProcessMemory()` to read what it wrote, followed by `VirtualFree()` so that I wouldn't leak memory in Explorer. Here is something similar: http://www.autohotkey.com/forum/topic5825.html – Daniel Trebbien Sep 21 '11 at 22:08
  • Ah. Well, I do not intend to make use of such features, so if there's a program that attempts something like this and it breaks things, that's acceptable to me. I think the bigger issue here is threads in my own process that use regular malloc. – cib Sep 21 '11 at 22:25
3

If you want to shrink an allocation, you can use VirtualFree with MEM_DECOMMIT on a subrange of the allocation. Note that this won't free up address space; only physical RAM. If you want to grow it, you can try VirtualAlloc passing an address immediately after your existing allocation. This may, of course, fail, at which point you need to copy memory.

You can also try using GlobalAlloc with GMEM_MOVEABLE and GlobalReAlloc (or the equivalent Heap* functions).

If you need to free address space, you might want to try using anonymous memory-mapping objects, and changing their mapped window at run-time - or simply use 64-bit to get additional address space.

bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • Doesn't help, he's out of address space. VirtualAlloc/Free has nothing to do with RAM on a demand-paged virtual memory operating system. – Hans Passant Sep 21 '11 at 19:11
  • @HansPassant, see my last suggestion - he can use anonymous memory-mapping objects, and map windows out of them. Or just go to 64-bit. – bdonlan Sep 21 '11 at 19:12
1

Not an answer, but I have to ask:

Considering the pain you're in, the performance hits of VirtualAlloc( ), and the non-portability of your code; as against any value VIrtualAlloc( ) gives, could you perhaps consider using malloc( ) and friends instead? IOW, does VirtualAlloc( ) confer any real advantage?

In my opinion (maybe only my opinion), the power and generality of malloc( ) outweigh any of the allure that VirtualAlloc( ) promises. And it would let you deal with your regions much more straightforwardly.

Sorry for the non-answer. I hate it when folks ask "who ever would even think to do that?" But of course it's all different when I'm the one asking "why" :-)

Pete Wilson
  • 8,610
  • 6
  • 39
  • 51
  • 1
    Simple, I'm writing my own malloc-level memory manager _because malloc isn't efficient for what I'm doing_ – cib Sep 21 '11 at 18:33
  • @cib, in most cases, even if direct use of malloc is inefficient, indirect use of malloc (ie, malloc to get a large block that you further subdivide) is usually ok... – bdonlan Sep 21 '11 at 18:44
  • @bdonlan: This may be the case, but it sounds like a hack at best. Using malloc, you have little control over where and how you allocate. For instance, some of my requirements will probably include page-alignment and addresses in a certain address-range. Sure, some malloc implementations may work out, but since malloc is abstract, you can never be sure. – cib Sep 21 '11 at 18:55
  • 1
    @cib -- It's pretty easy to make the address returned by malloc( ) be aligned on a page or anything else. After all, VirtualAlloc( ) is doing it; why not you? I am leaning on this because, to tell the truth, you are nailing shut the coffin of portability when you call a proprietary function. You might say, "well, it's only gonna' be *used* under *Windows*, fer chrissake!" Yes, I've said that many times, only to kick myself later. – Pete Wilson Sep 24 '11 at 13:42