c++memoryvisual-c++memory-managementpage-fault

Operator new behaves differently in Debug mode than in Release mode in MSVC


While testing some things regarding page faults I discovered a curious difference between how new operates in Debug mode and Release mode in MSVC. Consider the following code1:

#include <array>

constexpr size_t PAGE_SIZE = 4096;

int main()
{
    const size_t count = 1000000;
    char* const mem = new char[PAGE_SIZE * count];

    // page align the pointer, c-style casts used for brevity
    auto* pages = (std::array<char, PAGE_SIZE>*)((size_t)mem - (size_t)mem % PAGE_SIZE + PAGE_SIZE);
    
    for (int i = 0; i < count; ++i)
        pages[i][0] = 'a';
}

The code allocates a million normal memory pages on most architectures. It then physically writes to this allocated memory, so the memory really has to be "given"2 to the program - not merely "reserved" for it in some way. The curious thing is, when this actually happens. To investigate this, I stepped through the code using the Visual Studio debugger and looked at the memory usage graph in Task Manager. The results are below:

Measurement results

The red time point is the program being launched, the green time point/interval is the call to new char[], the blue time point/interval is the for loop.

As it turns out, in Debug mode, new both "reserves" and "gives" memory to the program. Meanwhile, in Release mode, it only "reserves" it, as the memory is "given" by the loop. I expected only the behavior present in Release mode - I thought that the memory is "given" to the program only when a page fault occurs.

Why does new behave in this way? Does this have any significant implications?


1 By the way, for some reason changing auto* pages to auto* const pages causes an Internal Compiler Error.

2 I am a bit confused regarding the correct terminology, so I used "given" and "reserved" instead.


Solution

  • To understand what happened you need to know two things:

    1. The debug builds do a lot of cool stuff for you to help you find bugs. One is writing a known value into the program's memory so you'll more easily recognize that you've messed around with uninitialized storage.
    2. Modern memory management systems in CPUs are complicated, but one thing they all tend to do is as little as possible until they have to. When a program requests storage the underlying system checks that there is enough virtual addressing space and then almost always allows the request without filling it. No physical memory is found and assigned to the virtual memory. When the memory is accessed, then physical memory will be found and assigned or the program fails because memory was not available.

    The combination of points 1 and 2 mean the debug version of new acquires the memory and immediately accesses it by writing in the uninitialized memory detection pattern and forcing the system to find and hand over real memory in the green region. As an added bonus if the computer does run out of physical storage, the program will likely crash here and not some seemingly random point in the future when the request cannot be satisfied.

    The release version of new does not do point 1, so physical memory acquisition is deferred as per point 2. new exits the green region quickly without any physical memory. If some or all of the requested memory is never used, the computer profits by never having to do the work fulfilling the request. The program does use the requested storage in the for loop, so the system is forced to find and supply physical memory in the blue region.