为什么全局数组在只读模式下不消耗内存?

Why global arrays does not consume memory in readonly mode?

以下代码声明一个全局数组 (256 MiB) 并计算其项的总和。

该程序运行时消耗 188 KiB

#include <cstdlib>
#include <iostream>

using namespace std;

const unsigned int buffer_len = 256 * 1024 * 1024; // 256 MB
unsigned char buffer[buffer_len];

int main()
{
    while(true)
    {
        int sum = 0;
        for(int i = 0; i < buffer_len; i++)
            sum += buffer[i];

        cout << "Sum: " << sum << endl;
    }
    return 0;
}

下面的代码与上面的代码类似,但是在计算数组项的总和之前将数组元素设置为随机值。

此程序运行时消耗 256.1 MiB

#include <cstdlib>
#include <iostream>

using namespace std;

const unsigned int buffer_len = 256 * 1024 * 1024; // 256 MB
unsigned char buffer[buffer_len];

int main()
{
    while(true)
    {
        // ********** Changing array items.
        for(int i = 0; i < buffer_len; i++)
            buffer[i] = std::rand() % 256;
        // **********

        int sum = 0;
        for(int i = 0; i < buffer_len; i++)
            sum += buffer[i];

        cout << "Sum: " << sum << endl;
    }
    return 0;
}

为什么全局数组在只读模式下不消耗内存(188K 对 256M)?


更新:

在我的真实场景中,我将使用 xxd 命令生成缓冲区,因此它的元素不为零:

$ xxd -i buffer.dat buffer.cpp

评论中有很多猜测认为这种行为是由编译器优化解释的,但是OP对g++的使用(即没有优化)不支持这一点,同样架构上的汇编输出也不支持,这清楚地表明buffer 正在使用:

buffer:
        .zero   268435456
        .section        .rodata

...

.L3:
        movl    -4(%rbp), %eax
        cmpl    8435455, %eax
        ja      .L2
        movl    -4(%rbp), %eax
        cltq
        leaq    buffer(%rip), %rdx
        movzbl  (%rax,%rdx), %eax
        movzbl  %al, %eax
        addl    %eax, -8(%rbp)
        addl    , -4(%rbp)
        jmp     .L3

您看到此行为的真正原因是在内核的 VM 系统中使用了 Copy On Write。本质上,对于像您在这里这样的大型零缓冲区,内核将创建一个“零页面”并将 buffer 中的所有页面指向该页面。只有当页面被写入时才会被分配。

这在您的第二个示例中实际上也是如此(即行为相同),但是您正在用数据接触每一页,这会强制为整个缓冲区“调入”内存。尝试只写 buffer_len/2,您会看到进程分配了 128.2MiB 的内存。缓冲区真实大小的一半:

这里还有来自维基百科的有用摘要:

The copy-on-write technique can be extended to support efficient memory allocation by having a page of physical memory filled with zeros. When the memory is allocated, all the pages returned refer to the page of zeros and are all marked copy-on-write. This way, physical memory is not allocated for the process until data is written, allowing processes to reserve more virtual memory than physical memory and use memory sparsely, at the risk of running out of virtual address space.