堆栈内存未被释放

4
我有以下循环,它从我实现的C++并发队列中弹出元素。这是从这里实现的:https://juanchopanzacpp.wordpress.com/2013/02/26/concurrent-queue-c11/
while (!interrupted)
{
    pxData data = queue->pop(); 
    if (data.value == -1)
    { 
        break; // exit loop on terminating condition
     }
    usleep(7000); // stub to simulate processing
}

我正在使用CentOS7中的System Monitor查看内存历史记录。我想要在从队列中读取值之后释放占用的内存,但是当以下while循环运行时,我没有看到内存使用量下降。我已经验证了队列长度确实下降了。
当遇到-1并退出循环时,内存使用量确实下降了。(程序仍在运行) 但是我不能这样做,因为我想要在usleep的位置做一些密集处理。
问题:为什么不会释放数据占用的内存?(根据System Monitor) 堆栈分配的内存不应该在变量超出范围时被释放吗?
结构体如下所示,并在程序开始时填充。
typedef struct pxData
{
  float value; // -1 value terminates the loop
  float x, y, z;
  std::complex<float> valueData[65536];
} pxData;

这里有大约10000个pxData,大约相当于5GB的空间。系统只有大约8GB的内存。因此重要的是释放内存,以便系统可以进行其他处理。


它什么时候超出范围?usleep仍在作用域中运行。 - MatthewRock
我看不出为什么要用“C”标记这个。 - Sourav Ghosh
1
pop.value 是什么? - John Zwinck
我指的是usleep完成后while循环的下一个迭代。 pop.value是一个浮点数。 - lppier
抱歉...我在打字时看着另一个屏幕..没有网络连接。 - lppier
来自自由存储区的内存即使被管理它的对象释放,也不必立即变得可用。我想使用简单的 std::queue<float> 也会看到类似的情况。 - juanchopanza
2个回答

6

这里有几个因素需要考虑。

虚拟内存

首先,你需要明白的是,即使你的程序“使用”了5 GB的内存,也不意味着只剩下3 GB的RAM给其他程序使用。虚拟内存意味着这5 GB可能只有1 GB是实际的“常驻”数据,而另外4 GB实际上可能在磁盘上而不是在RAM中。因此,在查看程序时,查看“常驻集大小”而不是“虚拟大小”非常重要。请注意,如果你的系统真正缺乏RAM,操作系统可能会通过“分页”一些内存来缩小某些程序的RSS。因此,不要太担心系统监视器中出现的“5 GB” - 如果你有真正的具体性能问题,请担心。

堆分配

第二个方面是:为什么从队列中删除项目后,你的虚拟大小并没有减少。我们可以猜测你是通过一个接一个地用mallocnew创建它们,并将它们推到队列的末尾。这意味着你分配的第一个元素将首先从队列中出来。这反过来意味着当你排除了90%的队列时,你的内存分配可能是这样的:

[program|------------------unused-------------------|pxData]

这里的问题在于,现实世界中仅仅因为你 freedelete 一些东西,并不意味着操作系统立刻回收这些内存。事实上,除非空闲内存块处于“末尾”(即最近分配的),否则它可能无法回收任何未使用的区间。由于 C++ 没有垃圾回收机制并且不能在没有您允许的情况下移动内存中的项目,因此你的程序的虚拟内存中就会出现一个大的“空洞”。该空洞将用于满足未来的内存分配请求,但如果您没有这样的请求,它就会一直存在,直到队列完全为空:
[program|------------------unused--------------------------]

然后系统就能将您的虚拟地址空间缩小回来:

[program]

这让你回到了起点。

解决方案

如果您想要“修复”这个问题,一种选择是按照相反的顺序分配内存,即将最后分配的项目放在队列的前面。

另一个选择是通过mmap为队列分配元素,例如Linux将自动为“大型”分配执行此操作。您可以通过使用M_MMAP_THRESHOLD调用mallopt(3)并将其设置为比结构体大小小一点来更改此阈值。这使得分配彼此独立,因此操作系统可以单独回收它们。即使在不重新编译现有程序的情况下,也可以将此技术应用于现有程序,因此通常在需要解决无法修改的程序中出现此问题时非常有用。


感谢约翰的解释。"那个空洞将被用于满足未来的内存分配请求" 这是否意味着我目前使用usleep的处理程序能够利用这个"空洞"? - lppier
1
是的,如果您在之前的循环迭代中释放了一些内存,那么分配内存时malloc()new可能会重用虚拟内存(从“空洞”中)。当然,这假设有足够大的连续块可用于满足后来的请求(一旦队列被显着排空,可能会有这样的块)。 - John Zwinck
再次感谢。今天学到了新东西。 - lppier

4
一份 C++ 实现会调用一些 operator delete 来释放使用一些 operator new 动态分配的内存。在几个 C++ 标准库中,new 调用 malloc,而 delete 则调用 free(我从 Linux 角度出发,但其他操作系统的原则类似) 但是,尽管 malloc(或 ::operator new)有时会通过更改 虚拟地址空间(例如 mmap(2))的系统调用来向 OS 内核请求更多内存,但 free(或 ::operator delete)通常只是将释放的内存区域标记为可用于 未来 调用 malloc(或 new)。

就从内核角度来看(例如通过/proc/,参见proc(5)...),虚拟地址空间不会改变,即使在应用程序内部被标记为“释放”,并且将在未来的分配中重新使用(通过未来对mallocnew的调用),内存仍然被消耗。

而大多数 C++ 标准 容器 在内部使用堆数据。特别是您本地(栈分配)的 std::mapstd::vector(或 std::deque)变量将为内部数据调用 newdelete


顺便说一句,我觉得你的声明很奇怪。除非每个struct pxData都有恰好65536个已使用的valueData插槽,否则我建议使用一些std::vector来实现

  std::vector<std::complex<float>> valueData;

并相应地改进您的代码。您可能需要执行一些valueData.reserve(somesize); 和/或 valueData.resize(somesize); 和/或 valueData.push_back(somecomplexnumber);等操作...

1
在这种情况下,同时队列由std::queue支持,而std::queue又由std::deque支持。我的评论是,使用简单的std::queue<float>也会观察到相同的情况,即并发性是一个红鲱鱼。 - juanchopanza
你使用哪种特定的容器并不太重要。 - Basile Starynkevitch
是的,绝对正确。只想补充一些信息。 - juanchopanza
谢谢Basile,不管怎样65536是有意的,因为我正在读取数据的设备需要这个值。vector也可以使用。 - lppier
如果您需要每个pxData保留65536个复杂数字,且您有10000个实例,则至少需要655361000016字节。如果某些pxData实例仅可包含数千个复杂数字,则应使用一些容器,例如我建议的std::vector - Basile Starynkevitch
@BasileStarynkevitch 是的,我明白了。感谢您的建议。 - lppier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接