在分配的内存上 * 不 * 使用 free()可以吗?

我在学计算机工程,我有一些电子课程。我从我的两位教授(这些课程的教授)那里听说,有可能避免使用 free()函数(在 malloc()calloc()等之后) ,因为分配的内存空间可能不会再用于分配其他内存。也就是说,例如,如果您分配4个字节,然后释放它们,您将有4个字节的空间,可能不会再次分配: 您将有一个

我认为这是疯狂的: 你不能有一个 非玩具程序在堆上分配内存而不释放它。但是我没有足够的知识来解释为什么每个 malloc()都必须有一个 free()这么重要。

那么: 在什么情况下使用 malloc()而不使用 free()是合适的呢?如果没有,我怎么向我的教授解释呢?

8570 次浏览

简单: 只需阅读几乎所有半严肃的 malloc()/free()实现的源代码。这里,我指的是处理调用工作的实际 内存管理器。这可能在运行库、虚拟机或操作系统中。当然,在所有情况下,代码的可访问性并不相同。

通过将相邻的孔连接成较大的孔,确保内存不会碎片化,这是非常非常常见的。更严肃的分配器使用更严肃的技术来确保这一点。

因此,让我们假设您执行三次分配和取消分配,并按照以下顺序获取内存中的块:

+-+-+-+
|A|B|C|
+-+-+-+

个人分配的大小并不重要。然后你释放第一个和最后一个,A 和 C:

+-+-+-+
| |B| |
+-+-+-+

当你最终释放了 B,你(最初,至少在理论上)得到了:

+-+-+-+
| | | |
+-+-+-+

可以被分解成

+-+-+-+
|     |
+-+-+-+

也就是说,一个更大的空闲块,没有片段留下。

参考文献:

  • 尝试阅读 Dlmalloc的代码。我更先进,是一个完整的生产质量实现。
  • 即使在嵌入式应用程序中,碎片整理实现也是可用的。

这完全是无稽之谈,例如,有许多不同的 malloc实现,有些试图使堆更有效率,如 Doug Lea 的这个之一。

其他答案已经很好地解释了 malloc()free()的真实实现确实会将(碎片整理)漏洞合并成更大的空闲块。但即使情况并非如此,放弃 free()仍然是一个糟糕的主意。

问题是,您的程序刚刚分配(并想释放)这4个字节的内存。如果它要运行一段较长的时间,那么它很可能只需要再次分配4个字节的内存。因此,即使这4个字节永远不会合并成一个更大的连续空间,它们仍然可以被程序本身重用。

你们的教授提出了一个重要的观点。不幸的是,英语的用法是这样的,我不能完全确定他们说的是什么。让我回答这个问题,非玩具程序具有一定的内存使用特征,我个人与。

有些程序运行良好。它们以波的形式分配内存: 大量的小型或中型分配,然后是大量的空闲,重复循环。在这些程序中,典型的内存分配器表现得相当好。它们合并释放的块,在 wave 结束时,大部分空闲内存都是大块连续的。这些程序非常罕见。

大多数程序表现不好。它们或多或少地随机分配和释放内存,大小从非常小到非常大不等,并且它们保留了所分配块的高使用率。在这些程序中,合并块的能力是有限的,随着时间的推移,它们最终会使内存高度碎片化和相对不连续。如果32位内存空间中的总内存使用量超过1.5 GB,并且分配(比如说)为10 MB 或更多,那么最终其中一个大型分配将失败。这些程序很常见。

其他程序只能释放很少或根本没有内存,直到它们停止。它们在运行时逐步分配内存,只释放少量内存,然后停止,此时所有内存都被释放。编译器是这样的。虚拟机也是。例如,。NETCLR 运行时本身是用 C + + 编写的,可能永远不会释放任何内存。为什么?

这就是最终答案。在程序内存使用量足够大的情况下,使用 malloc 和 free 管理内存并不能充分解决这个问题。除非你足够幸运能够处理一个行为良好的程序,否则你将需要设计一个或多个自定义内存分配器来预先分配大块内存,然后根据你选择的策略进行子分配。除非程序停止,否则不能使用 free。

在不知道你们的教授到底说了什么的情况下,对于真正的生产规模计划,我可能会站在他们那边。

剪辑

我要试着回答一些批评。显然,SO 不是一个发布这类帖子的好地方。先说清楚: 我有大约30年的编写这类软件的经验,包括几个编译器。我没有学术资料,只有我自己的瘀伤。我情不自禁地感到,批评来自那些经验更为狭隘、更为短暂的人。

我将重复我的关键信息: 平衡 malloc 和 free 是 没有在实际程序中大规模内存分配的充分解决方案。块合并是正常的,可以争取时间,但是 没有已经足够了。您需要认真、聪明的内存分配器,它们倾向于以块的形式(使用 malloc 或其他形式)获取内存,很少释放内存。这可能是 OP 的教授们所想的信息,但他误解了。

你提到过他们是电子学教授。它们可能用于编写固件/实时软件,能够准确地计时某些通常需要执行的东西。在这些情况下,如果知道您有足够的内存来进行所有的分配,而不释放和重新分配内存,那么可以更容易地计算出执行时间的限制。

在一些方案中,硬件存储器保护也可用于确保例程在其分配的存储器中完成或在应该是 非常异常情况下产生陷阱。

从一个不同的角度来看,与以前的评论和回答不同,一种可能性是,你的教授有经验的系统,内存是静态分配(即: 当程序编译)。

当您执行以下操作时,会出现静态分配:

define MAX_SIZE 32
int array[MAX_SIZE];

在许多实时和嵌入式系统(那些最有可能遇到 EE 或 CE 的系统)中,通常最好完全避免动态内存分配。因此,使用 mallocnew和它们的删除对应物是罕见的。最重要的是,近年来计算机的内存出现了爆炸式增长。

如果您有512 MB 可用,并且您静态分配了1 MB,那么在软件爆炸之前,您大约有511 MB 要处理(好吧,不完全是... ... 但请跟我来)。假设您有511MB 可以滥用,如果您每秒钟处理4个字节而不释放它们,那么您将能够在内存耗尽之前运行近73个小时。考虑到许多机器每天关闭一次,这意味着您的程序永远不会耗尽内存!

在上面的示例中,泄漏为每秒4字节,或者每分钟240字节。现在假设您降低了字节/最小比率。这个比率越低,程序运行的时间就越长,没有问题。如果你的 malloc很少,这是一个真正的可能性。

见鬼,如果你知道你只会去 malloc的东西一次,而且这个 malloc将永远不会再次被击中,那么它很像静态分配,虽然你不需要知道它的大小,你正在分配预先。例如: 假设我们又有512MB。我们需要 malloc32个整数数组。这些是典型的整数-每个4字节。我们知道这些数组的大小永远不会超过1024个整数。程序中不会出现其他内存分配。我们有足够的记忆吗?32 * 1024 * 4 = 131072.128KB,所以是的。我们有足够的空间。如果我们知道我们将永远不会分配更多的内存,我们可以安全地 malloc这些数组而不释放它们。但是,这也可能意味着,如果程序崩溃,您必须重新启动机器/设备。如果您启动/停止程序4,096次,您将分配所有512MB。如果您有僵化的进程,那么即使在崩溃之后,内存也可能永远不会被释放。

拯救你自己的痛苦和不幸,并消费这个咒语作为一个真理: malloc应该 一直都是free相关联。new应该有 一直都是delete

你的教授有没有可能在 POSIX 工作?如果他们习惯于编写大量小型的、极简的 shell 应用程序,那么我可以想象这种方法不会太糟糕——在操作系统 的空闲时间立即释放整个堆比释放一千个变量更快。如果您希望应用程序运行一两秒钟,那么您可以轻松地完成任务,根本不需要解除分配。

当然,这仍然是一种糟糕的做法(性能改进应该始终基于分析,而不是模糊的直觉) ,在没有解释其他约束的情况下,这不是你应该对学生说的话,但我可以想象许多微型管道 shell 应用程序都是这样编写的(如果不完全使用静态分配的话)。如果您正在从不释放变量中获益,那么您要么是在极低延迟的条件下工作(在这种情况下,您如何负担得起动态分配和 C + + ?: D) ,或者你正在做一些非常非常错误的事情(比如通过一个接一个地分配一千个整数而不是一个内存块来分配一个整数数组)。

我认为,如果从程序员的角度来看,这个问题中陈述的观点毫无意义,但从操作系统的角度来看,它是有道理的(至少有一些道理)。

Malloc ()最终将调用 mmap ()或 sbrk () ,后者将从操作系统获取页面。

在任何非平凡的程序中,即使您释放了()大部分分配的内存,在进程生存期内将该页面返回给 OS 的机会也是非常小的。因此,free ()’d 内存在大多数情况下只对同一个进程可用,而对其他进程不可用。

我感到惊讶的是,迄今为止还没有人引用 那本书的话:

这最终可能不是真的,因为内存可能会变得足够大,以至于在计算机的生命周期内不可能耗尽可用内存。例如,一年大约有3兆1013微秒,所以如果我们每微秒减少一次,我们需要大约1015单元的内存来建造一台可以运行30年而不耗尽内存的机器。以今天的标准来看,这么大的内存似乎大得离谱,但这在物理上并非不可能。另一方面,处理器的速度越来越快,未来的计算机可能会有大量的处理器在单个存储器上并行运行,因此,消耗存储器的速度可能比我们假设的要快得多。

Http://sarabander.github.io/sicp/html/5_002e3.xhtml#foot298

因此,事实上,许多程序可以做得很好,甚至不需要释放任何内存。

你的教授没有错,但也有错(他们至少是在误导或过于简单化)。内存碎片会导致性能和内存使用效率方面的问题,因此有时必须考虑并采取措施避免它。一个经典的技巧是,如果你分配了很多相同大小的东西,在启动时获取一个内存池,这是一些倍的大小,并管理它的使用完全内部,从而确保你没有发生碎片在操作系统级别(和你的内部内存映射器的漏洞将正好适合下一个对象的类型出现)。

有完整的第三方库只为您处理这类事情,有时候这是可接受性能和运行速度太慢之间的差异。malloc()free()需要花费大量的时间来执行,如果您调用它们的次数过多,就会开始注意到这一点。

因此,避免只是天真地使用 malloc()free(),你可以避免碎片和性能问题-但是当你正确地到它,你应该始终确保你 free()的一切你 malloc(),除非你有一个很好的理由去做其他。即使在使用内部内存池时,一个好的应用程序也会在退出之前 free()池内存。是的,操作系统会清理它,但是如果应用程序的生命周期稍后发生变化,就很容易忘记池仍然存在..。

长时间运行的应用程序当然需要非常小心地清理或回收它们分配的所有内存,否则它们最终会耗尽内存。

我知道有一种情况,显式释放内存是 比没用还糟糕。也就是说,当 你需要所有的数据,直到最后的进程生命周期。换句话说,只有在程序终止之前才可能释放它们。由于任何现代操作系统在程序死亡时都会小心翼翼地释放内存,因此在这种情况下不需要调用 free()。实际上,它可能会减慢程序终止的速度,因为它可能需要访问内存中的几个页面。