在 MSVC 中,Operator new 在 Debug 模式下的行为与 Release 模式下的行为不同

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

在测试有关页面错误的一些事情时,我发现了 new 在 MSVC 中在调试模式和发布模式下的运行方式之间的奇怪差异。考虑以下代码1:

#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';
}

该代码在大多数架构上分配了一百万个普通内存 pages。然后它物理地写入这个分配的内存,所以内存真的必须“给”2 给程序——而不仅仅是以某种方式“保留”给它。奇怪的是,当这真的发生时。为了对此进行调查,我使用 Visual Studio 调试器逐句检查了代码,并查看了任务管理器中的内存使用情况图。结果如下:

红色时间点是正在启动的程序,绿色时间点point/interval是调用new char[],蓝色时间点point/interval是for循环。

事实证明,在调试模式下,new 为程序“保留”和“提供”内存。同时,在释放模式下,它只“保留”它,因为内存是由循环“提供”的。我只期望发布模式中存在的行为 - 我认为只有在发生页面错误时才会将内存“分配”给程序。

为什么 new 会这样?这有什么重要意义吗?


1 顺便说一句,由于某些原因,将 auto* pages 更改为 auto* const pages 会导致内部编译器错误。

2我对正确的术语有点困惑,所以我用“给定”和“保留”代替。

调试 new 用可识别的模式填充内存。

要了解发生了什么,您需要了解两件事:

  1. 调试版本为您做了很多很酷的事情来帮助您找到错误。一种是将一个已知值写入程序的内存,这样您就可以更容易地识别出您在处理未初始化的存储。
  2. CPU 中的现代内存管理系统很复杂,但它们都倾向于做的一件事是尽可能少,直到不得不这样做。当程序请求存储时,底层系统会检查是否有足够的虚拟寻址 space,然后几乎总是允许请求而不填充它。没有找到物理内存并分配给虚拟内存。当内存被访问时,然后物理内存将被找到并分配或程序失败,因为内存不可用。

点1和点2的组合意味着new的调试版本通过写入未初始化内存检测模式并强制系统找到并移交内存中的真实内存来获取内存并立即访问它绿色区域。作为一个额外的好处,如果计算机 运行 物理存储空间不足,程序可能会在这里崩溃,而不是在未来无法满足请求时崩溃的某个看似随机的点。

new的release版本没有做到第1点,所以物理内存的获取按照第2点推迟。new在没有任何物理内存的情况下快速退出绿色区域。如果从未使用过部分或全部请求的内存,则计算机无需执行满足请求的工作即可获利。该程序确实在 for 循环中使用了请求的存储空间,因此系统被迫在蓝色区域中查找并提供物理内存。