内存需求:c++中的堆与堆栈

Memory Demands: Heap vs Stack in C++

本文关键字:堆栈 需求 c++ 内存      更新时间:2023-10-16

所以今晚我有一个奇怪的经历。

我正在用c++编写一个程序,该程序需要某种方式从文件中读取一长串简单数据对象,并将它们存储在主存中,大约有400,000个条目。对象本身类似于:

class Entry
{
public:
    Entry(int x, int y, int type);
    Entry(); ~Entry();
    // some other basic functions
private:
    int m_X, m_Y;
    int m_Type;
};

简单,对吧?因为我需要从文件中读取它们,所以我使用了像

这样的循环
Entry** globalEntries;
globalEntries = new Entry*[totalEntries];
entries = new Entry[totalEntries];// totalEntries read from file, about 400,000
for (int i=0;i<totalEntries;i++)
{
    globalEntries[i] = new Entry(.......);
}

当我在任务管理器上跟踪程序时,对程序的添加增加了大约25到35兆字节。对堆栈分配的一个简单更改:

Entry* globalEntries;
globalEntries = new Entry[totalEntries];
for (int i=0;i<totalEntries;i++)
{
    globalEntries[i] = Entry(.......);
}

突然只需要3兆字节。为什么会这样呢?我知道指针对象有一点额外的开销(4字节的指针地址),但它不应该足以产生那么大的差异。可能是因为程序分配内存效率不高,最终在已分配的内存之间出现了未分配的内存块?

你的代码是错误的,或者我不知道这是如何工作的。对于new Entry [count],您创建了一个新的Entry数组(类型为Entry*),但您将其分配给Entry**,因此我假设您使用的是new Entry*[count]

接下来要做的是在堆上创建另一个新的Entry对象,并将其存储在globalEntries数组中。所以你需要存储400.000个指针和400.000个元素。在64位机器上,400,000个指针占用3 MiB内存。此外,您有400.000个单条目分配,这些都需要sizeof (Entry)加上可能更多的内存(对于内存管理器来说——它可能必须存储分配的大小、相关池、对齐/填充等),这些额外的簿记内存可以很快地加起来。

如果您将第二个示例更改为:

 Entry* globalEntries;
 globalEntries = new Entry[count];
 for (...) {
     globalEntries [i] = Entry (...);
 }

内存使用应该等于堆栈方法。

当然,理想情况下您将使用std::vector<Entry>

首先,如果没有指定您正在查看的列,那么任务管理器中的数字没有任何意义。在现代操作系统中,甚至很难定义您所说的"已用内存"是什么意思—我们是在谈论私有页面吗?工作集?只保留在内存中的东西吗?保留但未提交的内存计数吗?谁为进程之间的内存共享买单?是否包括内存映射文件?

如果你正在观察一些有意义的度量,不可能看到3 MB的内存使用-你的对象至少是12字节(假设32位整数并且没有填充),所以400000个元素将需要大约4.58 MB。此外,如果它与堆栈分配一起工作,我会感到惊讶- vc++中的默认堆栈大小是1 MB,你应该已经有堆栈溢出了。

无论如何,期望不同的内存使用是合理的:

  • 堆栈(大部分)从一开始就分配了,所以这是你名义上消耗的内存,即使没有真正使用它做任何事情(实际上虚拟内存和自动堆栈扩展使这有点复杂,但这是"足够真实的");
  • CRT堆对任务管理器是不透明的:它所看到的是操作系统给进程的内存,而不是C堆"真正"使用的内存;堆的增长(向操作系统请求内存)超过了为进一步的内存请求做好准备的严格需要——所以你看到的是在没有进一步的系统调用的情况下,它准备放弃多少内存;
  • 你的"单独分配"方法有很大的开销。使用new Entry[size]获得的全连续数组花费size*sizeof(Entry)字节,加上堆簿记数据(通常是一些整数大小的字段);分隔分配方法的成本至少是size*sizeof(Entry)(所有"裸元素"的大小)加上size*sizeof(Entry *)(指针数组的大小)加上size+1乘以每次分配的成本。如果我们假设一个32位体系结构,每次分配的成本为2 int,那么您很快就会发现,这需要size*24+8字节的内存,而不是堆中连续数组的size*12+8字节;
  • 堆通常会放弃不是你真正要求的大小的块,因为它管理固定大小的块;所以,如果你像这样分配单个对象,你可能还需要支付一些额外的填充——假设它有16个字节的块,你需要为每个元素额外支付4个字节,通过单独分配它们;这将内存估计移至size*28+8,即每个12字节元素的开销为16字节。