释放内存需要时间吗?

15
我有一个C++程序,在执行期间会分配约3-8GB的内存来存储哈希表(我使用tr1/unordered_map)和其他各种数据结构。
然而,在执行结束后,会出现长时间的暂停才返回到shell。
例如,在我的主函数的最后,我有以下代码:
std::cout << "End of execution" << endl;

但是我的程序执行大概会像这样:

  

$ ./program
  做些事情...
  执行结束
  [长达2分钟的暂停]
  $ -- 返回到shell

这是预期的行为还是我做错了什么?

我猜测程序在结尾处正在释放内存。但是,使用大量内存(例如Photoshop)的商业应用程序在关闭应用程序时不会出现此暂停。

请给建议 :)

编辑:最大的数据结构是一个用字符串作为键的unordered_map,并存储一个整数列表。

我在Linux上使用g++ -O2,我使用的计算机有128GB的内存(其中大部分空闲)。有一些巨大的对象

解决方案:最终我放弃了哈希表,因为它几乎已满。这解决了我的问题。


2
你使用的是哪个平台/编译器?编译时使用了哪些标志?内存中的对象是否有析构函数?这些析构函数做了什么?有很多小对象还是少量大对象? - James McNellis
1
预先分配一大块内存,像栈一样使用它可能更有效。然后你可以一次性释放所有内存。如果程序退出时大多数对象都被释放,这种方法至少会更好。 - Alex Jasmin
2
两分钟?那么你很幸运:有些人说他们遇到了长达几个小时的删除时间——请参见此处的第4条评论:http://stackoverflow.com/questions/1354958/memory-leaks-in-c-via-newdelete/1354986#comment-1193133 - P Shved
2
我怀疑作者创造了一个虚假问题,只是为了让其他人吃下钉子,并梦想着拥有128GiB的内存。 - bohdan_trotsenko
1
可能是分配的数量而不是实际使用的数据量... - jcoder
显示剩余6条评论
16个回答

14

如果数据结构在程序结束时足够复杂,则释放它们可能需要很长时间。

如果您的程序确实必须创建这种复杂的结构(进行一些内存分析以确保),那么可能没有干净的途径可避免这种情况。

可以通过一个不好的方法来缩短内存释放的时间 - 至少在那些自动在进程终止时释放进程分配的所有内存的操作系统中。

您可以直接调用libc的exit(3)函数或操作系统的_exit(2)。 但是,我会非常小心地验证这不会使C ++析构器代码执行的任何其他(重要的)清理操作短路。 这样做或不做取决于高度系统依赖性(操作系统、编译器、libc、您正在使用的API等)。


exit(2) 会有什么问题?假设我没有任何特殊的析构函数。这可能会留下无法再次分配的内存吗? - jm1234567890
2
调用exit()会终止你的应用程序并释放其内存,但可能不会释放打开的文件句柄,这将迫使你重新启动系统。我在Solaris上见过这种情况发生。此外,假设没有特殊的析构函数是毫无意义的。必须是你的析构函数出了问题。我几乎可以确定。 - luis.espinal
exit()会释放内存和文件描述符,但是如果你有执行类似于删除临时文件的析构函数,那么如果你调用exit()可能不会发生。 - mark4o
我认为在某些实现中,exit()会调用全局析构函数。你可能需要使用_exit()代替。 - Zan Lynx
请确保您的文件已关闭,以刷新未写入的I/O。 - Potatoswatter
1
exit() 函数将调用全局和静态对象的析构函数,但不会调用堆栈或堆上对象的析构函数。_exit() 函数根本不会调用任何析构函数。无论哪种方式,操作系统都将确保回收所有内存。 - mark4o

8

是的,释放内存可能需要一些时间,还有可能会执行像析构函数这样的代码。Photoshop不使用3-8GB的内存。

此外,您应该考虑向你的应用程序添加分析功能来确认是否是内存释放或其他原因引起的问题。


将分配约3-8Gb的内存。 单位为千兆位,而不是千兆字节。所以他说的是375-1000 MB。 - David Callanan

7

我开始是作为对ndim的回复,但是它变得太长了。

正如ndim已经发表的,终止可能需要很长时间。
可能的原因包括:

  • 您有大量分配,堆的部分被交换到磁盘上。
  • 长时间运行的析构函数
  • 其他atexit例程
  • 操作系统特定的清理,例如在Windows上通知DLL线程和进程终止(不知道Linux上会发生什么)。

exit在这里不是最糟糕的解决方法,但实际行为取决于系统。例如,在Windows / MSVC CRT上,exit将运行全局析构函数/atexit例程,然后调用ExitProcess关闭句柄(但不一定刷新它们-至少不能保证)。

缺点:堆分配对象的析构函数不运行-如果您依赖它们(例如保存状态),则无法使用。此外,跟踪真正的内存泄漏变得更加困难。

找出原因您应该首先分析正在发生的情况。

例如,通过手动释放仍然分配的根对象,您可以将释放时间与其他进程清理分离。根据您的描述,内存是可能的原因,但不是唯一可能的原因。一些清理代码在运行到超时之前死锁也是可能的。监视统计数据(例如CPU /交换活动/磁盘使用)可以提供线索。

检查发布版本-调试版本通常使用堆上的额外数据,这可能会极大地增加清理成本。

不同的分配器
如果释放是问题所在,则您可能会从使用自定义分配机制中受益很多。例如:如果您的映射仅增长(项目永远不会被删除),则arena allocator可以帮助很多。如果您的整数列表有许多节点,请切换到vector,或者如果需要随机插入,请使用rope。


1
在编程中,使用自定义分配器可以获得额外的加分。我认为,通过正确使用分配器和对象池技术,系统回收内存的次数可以减少到不再引起问题的程度,从而消除了需要使用hacky exit calls的必要性。 - Necrolis

6
当然可以。
大约7年前,我在一个项目上遇到了类似的问题,当时内存要少得多,但我想电脑速度也更慢吧。
最终我们不得不查看免费的汇编语言,以找出为什么它如此缓慢,似乎它基本上是将已释放的块保留在链接列表中,以便可以重新分配,并且还在扫描该列表以查找要组合的块。扫描列表是一个O(n)操作,但释放'n'对象将其变成O(n^2)。
我们的测试数据需要大约5秒钟来释放内存,但一些客户拥有比我们使用的数据多10倍的数据,关闭他们系统上的程序需要5-10分钟。
我们修复了它,就像建议的那样,只需终止进程,让操作系统清理混乱(我们知道在我们的应用程序上这样做是安全的)。
也许你有比几年前更明智的免费功能,但我只是想发帖说,如果您有许多要释放的对象和O(n)的免费操作,那么完全有可能。

5

我无法想象你如何使用足够的内存使其成为问题,但我加速程序的一种方法是使用 boost::object_pool 给二叉树分配内存。对我来说,主要好处是我可以将对象池放置为树的成员变量,当树超出范围或被删除时,对象池将一次性删除(让我不必使用递归析构函数来处理节点)。object_pool 在退出时确实调用了所有对象的析构函数。我不确定它是否以特殊方式处理空析构函数。

如果您的分配器不需要调用构造函数,您还可以使用 boost::pool,我认为它可能更快地释放内存,因为它根本不需要调用析构函数,只需在一个 free() 中删除内存块。


4

释放内存可能需要一定时间 - 数据结构正在更新。这需要取决于所使用的分配器。

此外,可能不仅仅是内存释放 - 如果正在执行析构函数,则可能会发生更多情况。

虽然2分钟听起来很长,但您可能希望通过调试器(或者如果更方便的话,使用分析器)逐步执行清理代码,以查看实际花费了多少时间。


4
时间可能不完全浪费在释放内存上,而是调用所有析构函数。如果映射中的对象不需要被销毁,只需分配,则可以提供自己的分配器,该分配器不调用析构函数。
另请参阅此其他问题:C++ STL-conforming Allocators

3

通常情况下,进程结束时释放内存并不是进程的一部分,而是作为操作系统清理功能的一部分。您可以尝试使用valgrind来确保内存得到正确处理。但是,编译器还会执行某些操作来设置和拆除程序,因此进行某种性能分析或使用调试器逐步查看拆除时间发生的情况可能会有所帮助。


2
当您的程序退出时,将调用所有全局对象的析构函数。如果其中一个需要很长时间,您会看到这种行为。
查找全局对象并调查它们的析构函数。

1

很抱歉,这是一个糟糕的问题。您需要展示使用的特定算法和数据结构的源代码。

可能是正在解除分配,但这只是一个猜测。您的析构函数在做什么?也许像疯狂换页。只因为您的应用程序分配了X数量的内存,并不意味着它将获得它。最有可能会将其分页到虚拟内存中。根据您的应用程序和操作系统的具体情况而定,您可能会出现大量页面故障。

在这种情况下,运行iostat和vmstat可能有所帮助,以查看到底发生了什么。如果您看到大量I/O,那么您正在出现页面故障的迹象。I/O操作的成本始终比内存操作更高。

如果所有经过的时间纯粹由于解除分配,则我会非常惊讶。

在收到“结束”消息后立即运行vmstat和iostat,并查找任何I/O疯狂的迹象。


谢谢!我也会看一下。我已经更新了问题并提供了具体的数据结构。 - jm1234567890

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