1

The Microsoft documentation describes the behavior of VirtualAlloc() when used with the flag MEM_RESERVE:

Reserves a range of the process's virtual address space without allocating any actual physical storage in memory or in the paging file on disk.

Does VirtualAlloc() used with MEM_RESERVE produces a side effect that indirectly increases the overall memory consumption of the process?

Even if the reserved virtual pages are not associated to physical pages, some information about them could still be stored individually in a data structure, and each reserved virtual page could consume a couple of bytes of physical memory. That would be a problem if someone wants to reserve a huge amount of virtual memory (13 TiB for instance).

You can test if VirtualAlloc() used with MEM_RESERVE consumes memory indirectly with this test program:

#include <windows.h>
#include <stdio.h>
#include <stddef.h>

int main(void) {
    void *address = VirtualAlloc(NULL, 13ull << 40ull, MEM_RESERVE, PAGE_NOACCESS);
    if (address == NULL)               
        printf("Reservation failure.\n");
    getchar();
    return 0;
}

If you run this program, please indicate which compiler and version of Windows you are using.

RalphS
  • 627
  • 4
  • 15
  • 1
    Cannot say for sure, but I guess WINE is playing tricks with any OS calls. What happens if you run the 'test' on a native Windows platform? – Adrian Mole Dec 04 '20 at 07:35
  • I do not have a native Windows platform to run this test. I only have GNU/Linux. – RalphS Dec 04 '20 at 07:40
  • 1
    OK - I'm busy just now getting ready to go to work. I'll try to run your code when I get some time, and let you know what happens (if I can). – Adrian Mole Dec 04 '20 at 08:04
  • That would be very kind of you. Thanks. – RalphS Dec 04 '20 at 08:06
  • You can `strace(1)` WINE to see what system calls it is actually making to Linux. But Microsoft's docs describe what native Windows would do, and I wouldn't assume that WINE's behavior is necessarily similar. It's entirely possible that its behavior is much less efficient. – Nate Eldredge Dec 04 '20 at 23:36
  • CFG uses this trick to implement security. Reserving TB of address space is a non issue. See e.g. https://stackoverflow.com/questions/56909785/unreasonably-huge-process-virtual-memory-size-reported-by-process-explorer. Whatever you did measure with Wine is not even similar to what happens in Windows. – Alois Kraus Dec 04 '20 at 23:39
  • @NateEldredge What system calls Wine makes internally is not really important to me, because my concern is the behavior of the program when executed on Windows. I hope that the behavior of VirtualAlloc is more efficient on Windows, but I don't have a native Windows installation to test it. – RalphS Dec 04 '20 at 23:49
  • 1
    I don't think WINE is going to be a satisfactory substitute for testing on Windows itself. You can get free Windows VMs from Microsoft for testing / development purposes, see https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/. – Nate Eldredge Dec 04 '20 at 23:56
  • @AloisKraus Do you mean that the problem I observed does not exist on a native Windows environment? – RalphS Dec 05 '20 at 00:05
  • @RalphS: Yes this is a non issue on "real" Windows. – Alois Kraus Dec 05 '20 at 18:48

2 Answers2

2

Do you know if this also happens on a native Windows environment?

On native Windows 8 (compiled by Mingw64, with stats taken from task manager); your code consumes 25.6 MB of physical memory. By changing it to "size_t bytes = 1;" the physical memory consumption changes to 0.2 MB; which implies that the VirtualAlloc() itself is responsible for about 25.4 MB of physical memory.

Note that some physical memory is needed to keep track of which area/s are reserved; and because you can free/allocate individual pages independently this is likely to be more than just a "start and end for entire area" (and might be a separate marker for every 1 GiB or 2 MiB or 4 KiB piece in the area, to coincide with the structure of tables that the CPU uses for "virtual address to physical address" conversion).

EDIT

Based on comments (repeating tests for different sizes of virtual memory), I slapped together this mess (a loop that doubles the size being allocated and reports the results for each size that worked):

#define __USE_MINGW_ANSI_STDIO 1

#include <windows.h>    /* VirtualAlloc() */
#include <psapi.h>
#include <stdio.h>      /* getchar() */
#include <stddef.h>     /* size_t */

int main(void) {
    HANDLE hProcess;
    PROCESS_MEMORY_COUNTERS pmc;
    void *address;
    size_t size = 1;

    hProcess = GetCurrentProcess();
    if (NULL == hProcess) {
        printf( "OpenProcess() failed\n");
        getchar();
        return 1;
    }

    while( (address = VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS)) != NULL) {
        if ( GetProcessMemoryInfo( hProcess, &pmc, sizeof(pmc)) ) {
            printf( "Size %zu = Working set size: %u\n", size, pmc.WorkingSetSize );
        }
        VirtualFree(address, size, MEM_RELEASE);

        if(size > SIZE_MAX / 2) {
            break;
        }
        size += size;
    }

    CloseHandle( hProcess );
    getchar();
    return 0;
}

On my computer the output is:

Size 1 = Working set size: 1560576
Size 2 = Working set size: 1597440
Size 4 = Working set size: 1597440
Size 8 = Working set size: 1597440
Size 16 = Working set size: 1597440
Size 32 = Working set size: 1597440
Size 64 = Working set size: 1597440
Size 128 = Working set size: 1597440
Size 256 = Working set size: 1597440
Size 512 = Working set size: 1597440
Size 1024 = Working set size: 1597440
Size 2048 = Working set size: 1597440
Size 4096 = Working set size: 1597440
Size 8192 = Working set size: 1597440
Size 16384 = Working set size: 1597440
Size 32768 = Working set size: 1597440
Size 65536 = Working set size: 1597440
Size 131072 = Working set size: 1597440
Size 262144 = Working set size: 1597440
Size 524288 = Working set size: 1597440
Size 1048576 = Working set size: 1597440
Size 2097152 = Working set size: 1597440
Size 4194304 = Working set size: 1597440
Size 8388608 = Working set size: 1597440
Size 16777216 = Working set size: 1597440
Size 33554432 = Working set size: 1597440
Size 67108864 = Working set size: 1597440
Size 134217728 = Working set size: 1597440
Size 268435456 = Working set size: 1597440
Size 536870912 = Working set size: 1597440
Size 1073741824 = Working set size: 1601536
Size 2147483648 = Working set size: 1605632
Size 4294967296 = Working set size: 1613824
Size 8589934592 = Working set size: 1630208
Size 17179869184 = Working set size: 1662976
Size 34359738368 = Working set size: 1728512
Size 68719476736 = Working set size: 1859584
Size 137438953472 = Working set size: 2121728
Size 274877906944 = Working set size: 2646016
Size 549755813888 = Working set size: 3694592
Size 1099511627776 = Working set size: 5791744
Size 2199023255552 = Working set size: 9986048
Size 4398046511104 = Working set size: 18374656
Size 8796093022208 = Working set size: 35151872
Size 17592186044416 = Working set size: 68706304
Size 35184372088832 = Working set size: 135815168
Brendan
  • 35,656
  • 2
  • 39
  • 66
  • I changed the test program in my question. For the record, the value you give (25.6 MB of memory consumption) was obtained when reserving 13008 GiB of virtual memory with `VirtualAlloc()`. – RalphS Dec 05 '20 at 04:33
  • Your answer is good! You could make it _great_ by performing another couple of executions, for instance by using the following sizes: 1 TiB and 50 TiB. It would allow us to see if the percentage of overhead increases when the size of the reserved area increases. For exemple, if you say that reserving 13008 GiB of virtual memory consumes 25.6 MB of physical memory, then the overhead percentage is `25.6 / (13008*1024) == 0.00019%`. – RalphS Dec 05 '20 at 04:54
  • 1
    @RalphS: Done :-) – Brendan Dec 05 '20 at 05:13
0

Reserving memory has a cost of 1 bit per 64 KiB. It is never freed, even if we release the area. Remark: the granularity of VirtualAlloc(_, _, MEM_RESERVE, _) is 64 KiB.

Committing memory has a cost of 8 bytes per 4 KiB. It is consumed even if the memory is untouched, and it is freed only when we release the whole reserved area (a decommit is not enough). Remark: the size of a pointer is 8 bytes and the granularity of VirtualAlloc(_, _, MEM_COMMIT, _) is 4 KiB (the page size).

This has been tested on Windows 8.1 (64 bits) and Windows 10 (64 bits), with Mingw-w64 as compiler. It surprised me because on GNU/Linux, we can reserve and commit the entire virtual address space with mmap() without noticing any increase in memory usage.

RalphS
  • 627
  • 4
  • 15