【问题标题】:Stack memory not released堆栈内存未释放
【发布时间】:2015-12-15 08:03:49
【问题描述】:

我有以下循环,它从这里的实现中弹出我拥有的 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 中的系统监视器查看内存历史记录。 从队列中读取值后,我正在尝试释放队列占用的内存。但是,随着以下 while 循环的运行,我没有看到内存使用量下降。我已经验证队列长度确实下降了。

但是,当遇到 -1 并且循环退出时,它确实会失败。 (程序还在运行)但是我不能有这个,因为usleep在哪里,我想做一些密集的处理。

问题:为什么数据占用的内存没有被释放? (根据系统监视器)当变量超出范围时,堆栈分配的内存不应该被释放吗?

结构体定义如下,并在程序开始时填充。

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 仍在作用域中运行。
  • 我看不出有任何理由用C标记这个
  • 什么是pop.value
  • 我的意思是 while 循环的下一次迭代,在 usleep 完成之后。 pop.value 是一个浮点数。
  • 对不起...我正在打字看着另一个屏幕..没有互联网访问。

标签: c++ memory memory-management


【解决方案1】:

这里有一些事情在起作用。

虚拟内存

首先,您需要了解,仅仅因为您的程序“使用”了 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) 并将其设置为比您的结构大小小一点来更改此阈值。这使得分配彼此独立,因此操作系统可以单独回收它们。这种技术甚至可以在不重新编译的情况下应用于现有程序,因此如果您需要在无法修改的程序中解决此问题,这通常很有用。

【讨论】:

  • 感谢约翰的解释。 “那个洞将被用来满足未来的内存分配请求”这是否意味着我目前正在处理的东西将能够利用这个“洞”?
  • 是的,如果您在之前的循环迭代中释放了一些内存之后分配内存,malloc()new 可能会重用虚拟内存(来自“洞”)。这当然假设有足够大的连续块可用于满足后面的请求(一旦队列显着耗尽,可能会有)。
  • 再次感谢。今天学到了一些新东西。
【解决方案2】:

C++ 实现会调用一些operator delete 来释放动态分配的(使用一些operator new)内存。在几个 C++ 标准库中,new 调用 mallocdelete 调用 free

(我关注的是 Linux 的观点,但其他操作系统的原理类似)

但是,虽然malloc(或::operator new有时通过更改virtual address space(如mmap(2)free(或@987654335)的系统调用来向操作系统内核请求更多内存@) 经常只是简单地将已释放的内存区域标记为将来malloc(或new)的调用可重新使用

所以从内核的角度来看(例如,通过/proc/ 看到,参见proc(5)...),虚拟地址空间没有改变,并且内存仍然被消耗,即使在应用程序内部它被标记作为“已释放”,并将在将来的某些分配中重复使用(通过将来调用 mallocnew

而且大多数 C++ 标准 containers 都是在内部使用堆数据。特别是您的本地(堆栈分配)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); 等......

【讨论】:

  • 仅供参考,在这种情况下,并发队列由std::queue 备份,而std::deque 又由std::deque 备份。我对 OP 的评论是,他们会通过简单的std::queue&lt;float&gt; 观察到相同的情况,即并发是一条红鲱鱼。
  • 您使用哪个特定容器并不重要
  • 是的,绝对正确。只是想添加一些信息。
  • 感谢 Basile,无论如何 65536 是故意的,因为我正在从中读取数据的设备。矢量也可以工作。
  • 如果您需要为每个 pxData 保留 65536 个复数,并且如果您有 10000 个实例,则至少需要 65536*10000*16 个字节。如果某些pxData 实例可能只有一千个复数,您应该使用一些容器,例如我建议的std::vector
猜你喜欢
  • 2016-07-25
  • 1970-01-01
  • 2012-09-17
  • 2011-05-23
  • 2018-11-29
  • 2016-08-05
  • 2018-06-01
  • 2011-07-25
  • 2015-08-13
相关资源
最近更新 更多