为什么C中的`free`不占用要释放的字节数?

时间:2014-06-13 11:10:26

标签: c memory-management malloc free

为了清楚起见:我确实知道{C}库中实现了mallocfree,它通常从操作系统中分配大块内存并自行管理以分配较小的批量内存到应用程序并跟踪分配的字节数。这个问题不是How does free know how much to free

相反,我想知道为什么free首先是以这种方式制作的。作为一种低级语言,我认为要求C程序员不仅要跟踪分配的内存而且要跟踪多少内容(事实上,我通常发现我最终会跟踪字节数)是完全合理的无论如何都是malloced)。对我来说,明确给出free的字节数可能会允许一些性能优化,例如对于不同的分配大小具有单独池的分配器将能够通过查看输入参数来确定要释放哪个池,并且整体上的空间开销会更少。

因此,简而言之,为什么要创建mallocfree,以便他们需要在内部跟踪分配的字节数?这只是一次历史事故吗?

小编辑: 有些人提供了类似于"如果你获得的金额与你分配的金额不同的话会怎么样?#34;。我想象的API可能只需要一个就可以完全释放分配的字节数;释放更多或更少可能只是UB或实现定义。不过,我不想阻止有关其他可能性的讨论。

12 个答案:

答案 0 :(得分:92)

单参数free(void *)(在Unix V7中引入)与早先的两个参数mfree(void *, size_t)相比具有另一个主要优势,我在这里没有提到过:一个参数free大大简化了与堆内存一起使用的每个其他 API。例如,如果free需要内存块的大小,那么strdup将以某种方式返回两个值(指针+大小)而不是一个(指针),并且C使得多值返回更多比单值返回更麻烦。而不是char *strdup(char *)我们必须写char *strdup(char *, size_t *)struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)。 (现在第二个选项看起来很诱人,因为我们知道NUL终止的字符串是"most catastrophic design bug in the history of computing",但事后才说。回到70年代,C的能力处理字符串作为一个简单的char *实际上被认为是defining advantage over competitors like Pascal and Algol。)另外,它不仅仅是strdup遇到这个问题 - 它影响到每个系统或用户 - 定义了分配堆内存的函数。

早期的Unix设计人员非常聪明,free优于mfree的原因有很多,所以基本上我认为问题的答案是他们注意到这一点并相应地设计了他们的系统。我怀疑你在做出这个决定的那一刻,能够找到他们头脑中发生的事情的直接记录。但我们可以想象。

假装您正在用C语言编写应用程序以在V6 Unix上运行,并使用其两个参数mfree。到目前为止,您已经管理好了,但是跟踪这些指针大小变得越来越麻烦,因为您的程序become more ambitious并且需要越来越多地使用堆分配的变量。但是你有一个好主意:不是一直复制这些size_t,你可以写一些实用函数,它们直接在分配的内存中存放大小:

void *my_alloc(size_t size) {
    void *block = malloc(sizeof(size) + size);
    *(size_t *)block = size;
    return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
    block = (size_t *)block - 1;
    mfree(block, *(size_t *)block);
}

使用这些新功能编写的代码越多,它们看起来就越棒。它们不仅使您的代码更容易编写,而且使您的代码更快 - 这两件事情经常不在一起!在将这些size_t传递到整个地方之前,这会增加复制的CPU开销,并且意味着您必须更频繁地溢出寄存器(尤其是额外的函数参数),并浪费内存(因为嵌套函数调用通常会导致size_t的多个副本存储在不同的堆栈帧中。在新系统中,您仍然需要花费内存来存储size_t,但只能存储一次,而且它永远不会被复制到任何地方。这些似乎效率很低,但请记住,我们正在讨论具有256 KiB RAM的高端机器。

这让你开心!因此,你与正在研究下一个Unix版本的胡子男人分享你的酷炫伎俩,但它并没有让他们开心,这让他们感到难过。你看,他们只是在添加一些新的实用程序函数,如strdup,他们意识到使用你的酷技巧的人不能使用他们的新功能,因为他们的新功能都使用繁琐的指针+大小API。然后让你伤心过,因为你意识到你'将不得不重新编写好的strdup(char *)功能自己在每次你写的程序,而不是能够使用的系统版本

但是等等!这是1977年,向后兼容性再创造5年!此外,没有人认真地使用这个不起眼的" Unix"它的颜色名称的东西。 K& R的第一版现在正在向出版商发展,但这没有问题 - 它在第一页上表示" C不提供直接处理复合对象的操作,例如字符串......没有堆..."。在历史的这一点上,string.hmalloc是供应商扩展(!)。所以,建议大胡子男人#1,我们可以改变他们,但我们喜欢;为什么我们不宣布你的棘手的分配器是官方分配器?

几天之后,Bearded Man#2看到了新的API并且说,嘿,等等,这比以前更好,但它仍然在每个分配中花费整个单词存储大小。他认为这是亵渎的下一件事。其他人都像他一样疯狂地看着他,因为你还能做什么呢?那天晚上他迟到了,发明了一个根本没有存储大小的新分配器,而是通过对指针值执行黑魔法位移来实时推断它,并在保持新API到位的同时交换它。新的API意味着没有人注意到交换机,但他们确实注意到第二天早上编译器使用的内存减少了10%。

现在每个人都很高兴:你得到的是更容易编写和更快的代码,有胡子的人#1可以编写一个人们真正使用的简单strdup,而有胡子的人#2 - 确信他已经获得了一点保持 - 回到messing around with quines。发货吧!

或至少,

答案 1 :(得分:31)

  

"为什么C中的free不占用要释放的字节数?"

因为不需要它,并且它无论如何都不会有意义

当您分配某些内容时,您希望告诉系统要分配多少字节(出于显而易见的原因)。

但是,当您已经分配了对象时,现在可以确定您获得的内存区域的大小。这是隐含的。它是一个连续的内存块。你无法释放它的一部分(让我们忘记realloc(),那不是它&## 39;无论如何),你只能解除整个事情。你不能解除分配X字节"要么 - 你要么从malloc()释放内存块,要么你不要。

现在,如果你想释放它,你可以告诉内存管理器系统:"这里是指针,free()它指向的块。" - 并且内存管理器将知道如何做到这一点,因为它隐含地知道大小,或者因为它甚至可能不需要大小。

例如,malloc()的大多数典型实现维护指向空闲和已分配内存块的指针的链接列表。如果您将指针传递给free(),它只会在"已分配"中搜索该指针。列表,取消链接相应的节点并将其附加到" free"名单。 它甚至不需要区域大小。当它可能尝试重新使用相关块时,它只需要该信息。

答案 2 :(得分:14)

C可能不是"抽象"作为C ++,但它仍然是对汇编的抽象。为此,最低级别的细节从等式中取出。这可以防止你不得不使用对齐和填充,这大部分会使你的所有C程序都不可移植。

简而言之,这是编写抽象的全部内容

答案 3 :(得分:14)

实际上,在古老的Unix内核内存分配器中,mfree()采用了size参数。 malloc()mfree()保留了两个数组(一个用于核心内存,另一个用于交换),其中包含有关空闲块地址和大小的信息。

在Unix V6之前没有用户空间分配器(程序只使用sbrk())。在Unix V6中,iolib包含一个带有alloc(size)free()调用的分配器,它没有使用size参数。每个内存块前面都有它的大小和指向下一个块的指针。当指向空闲列表时,指针仅用于空闲块,并在正在使用的块中重新用作块存储器。

在Unix 32V和Unix V7中,这被新的malloc()free()实现取代,其中free()没有采用size参数。实现是一个循环列表,每个块前面都有一个单词,其中包含指向下一个块的指针,以及" busy" (分配)位。因此,malloc()/free()甚至没有跟踪明确的大小。

答案 4 :(得分:9)

有五个原因让人想起:

  1. 很方便。它从程序员那里消除了一大堆开销,避免了一类非常难以跟踪的错误。

  2. 它开启了释放部分区块的可能性。但由于记忆管理人员通常希望获得跟踪信息,因此不清楚这意味着什么?

  3. 轻盈竞赛在轨道上是关于填充和对齐的点。内存管理的本质意味着分配的实际大小很可能与您要求的大小不同。这意味着free要求大小以及位置malloc必须更改以返回分配的实际大小。

  4. 目前尚不清楚传递大小是否有任何实际好处,无论如何。典型的内存管理器每个内存块有4-16个字节的头,其中包括大小。对于已分配和未分配的内存,此块头可以是通用的,并且当相邻块自由时,它们可以折叠在一起。如果您正在使调用者存储空闲内存,则可以通过在分配的内存中没有单独的大小字段来释放每个块大约4个字节,但是由于调用者需要将其存储在某处,因此可能无法获得该字段。但是现在信息分散在内存中而不是可预测地位于标题块中,无论如何这可能会降低操作效率。

  5. 即使 更有效率,你的程序根本不可能花费大量时间释放内存 ,这样做的好处是小。

  6. 顺便提一下,如果没有这些信息,您可以轻松实现不同大小项目的单独分配器的想法(您可以使用该地址来确定分配的位置)。这通常是在C ++中完成的。

    稍后添加

    另一个答案,相当荒谬,提出std::allocator作为free可以这样工作的证据,但事实上,它是free无法做到的原因的一个很好的例子。这样做。 malloc / free和std :: allocator之间有两个主要区别。首先,mallocfree面向用户 - 它们是为一般程序员设计的 - 而std::allocator旨在为标准库提供专业内存分配。这提供了一个很好的例子,说明我的第一点没有或根本不重要。由于它是一个图书馆,因此无论如何都难以对用户隐藏处理跟踪尺寸复杂性的困难。

    其次,std :: allocator 始终使用相同大小的项,这意味着它可以使用最初传递的元素数来确定有多少空闲。为什么这与free本身不同,这是说明性的。在std::allocator中,要分配的项目始终具有相同,已知,大小和始终相同的项目,因此它们始终具有相同的对齐要求。这意味着分配器可以专门用于在开始时简单地分配这些项的数组,并根据需要进行分配。您无法使用free执行此操作,因为无法保证返回的最佳大小是所要求的大小,相反,有时返回比调用者要求的更大的块更有效*因此要么用户或经理需要跟踪实际授予的精确大小。将这些类型的实现细节传递给用户是一种不必要的麻烦,对调用者没有任何好处。

    - *如果有人仍然难以理解这一点,请考虑这一点:典型的内存分配器将少量跟踪信息添加到内存块的开头,然后返回指针偏移量。这里存储的信息通常包括例如指向下一个空闲块的指针。让我们假设标题只有4个字节长(实际上比大多数真正的库小),并且不包含大小,那么想象一下,当用户要求时,我们有一个20字节的空闲块16字节块,一个天真的系统将返回16字节块,但随后留下一个4byte片段,永远不会被用来每次malloc被调用时浪费时间。如果相反,管理器只返回20字节块,那么它将保存这些混乱的片段,并且能够更清晰地分配可用内存。但是如果系统要在不跟踪大小本身的情况下正确地执行此操作,那么我们就要求用户跟踪 - 对于每个单个分配 - 如果要将其免费传递回来,则实际分配的内存量。相同的参数适用于不匹配所需边界的类型/分配的填充。因此,至多要求free取大小是(a)完全无用,因为内存分配器不能依赖传递的大小来匹配实际分配的大小,或者(b)无意义地需要用户跟踪任何合理的内存管理器可以轻松处理的真实大小的工作。

答案 5 :(得分:9)

  

为什么C中的free不占用要释放的字节数?

因为它不需要。此信息已在malloc / free执行的内部管理中可用。

以下是两个注意事项(可能会或可能不会对此决定做出贡献):

  • 为什么您希望函数接收不需要的参数?

    (这会使依赖于动态内存的所有客户端代码复杂化,并为您的应用程序添加完全不必要的冗余)。跟踪指针分配已经是一个困难的问题。跟踪内存分配以及相关大小会不必要地增加客户端代码的复杂性。

  • 在这些情况下,改变后的free函数会做什么?

    void * p = malloc(20);
    free(p, 25); // (1) wrong size provided by client code
    free(NULL, 10); // (2) generic argument mismatch
    

    不会免费(导致内存泄漏?)?忽略第二个参数?通过调用exit来停止应用程序?实现这一点会在应用程序中添加额外的故障点,对于您可能不需要的功能(如果需要,请参阅下面的最后一点 - “在应用程序级别实施解决方案”)。

  

相反,我想知道为什么免费是这样做的。

因为这是“正确”的方式。 API应该需要执行它的操作所需的参数,并且不超过

  

在我看来,明确地给出空闲的字节数可能允许一些性能优化,例如,对于不同的分配大小具有单独池的分配器将能够通过查看输入参数来确定要释放哪个池,并且总体上会有更少的空间开销。

实现这一目标的正确方法是:

    在malloc实现中的
  • (在系统级别) - 没有什么能阻止库实现者根据接收的大小编写malloc以在内部使用各种策略。

  • (在应用程序级别)通过将malloc和free包装在您自己的API中,并使用它们(在您的应用程序中的任何地方,您可能需要)。

答案 6 :(得分:5)

我只是将这个作为答案发布,不是因为它是你所希望的那个,而是因为我相信它是唯一合理的正确答案:

原本可能认为方便,此后无法改进 可能没有令人信服的理由。(但我很乐意删除它,如果显示它是不正确的。)

如果可能的话, 会带来好处:你可以预先分配一个你知道它的大小的大块内存,然后一次释放一点 - 而不是重复分配和释放一小块记忆。目前这样的任务是不可能的。


对于那些认为传递大小的很多(很多 1 !)你是如此荒谬:

我可以参考C ++对std::allocator<T>::deallocate方法的设计决策吗?

void deallocate(pointer p, size_type n);
  

n 指向的区域中的所有 T p对象都应在此次调用之前销毁。
    n 应与传递给allocate的值相匹配,以获取此内存。

我认为你将有一个相当“有趣”的时间来分析这个设计决定。


至于operator delete,事实证明2013 N3778提案(“C ++ Sized Deallocation”)也是为了解决这个问题。


1 只要查看原始问题下的评论,看看有多少人做出草率断言,例如“要求大小对于free电话来说完全没用” 以证明缺少size参数。

答案 7 :(得分:2)

每个&#34; malloc&#34; malloc和free是齐头并进的。被一个&#34;免费&#34;匹配。因此,完全有意义的是&#34; free&#34;匹配前一个&#34; malloc&#34;应该简单地释放由malloc分配的内存量 - 这是99%的情况下有意义的大多数用例。想象一下,如果全世界所有程序员都使用malloc / free,所有的内存错误都需要程序员跟踪malloc中分配的数量,然后记住释放它们。你谈到的场景应该在某种内存管理实现中使用多个mallocs / frees。

答案 8 :(得分:1)

我建议这样做是因为不必以这种方式手动跟踪大小信息(在某些情况下),也不太容易出现程序员错误。

此外,realloc需要这个簿记信息,我希望它不仅包含分配大小。即它允许其工作的机制被实现定义。

您可以编写自己的分配器,但这种分配器在您建议的方式上有所作为,并且通常在c ++中为池分配器以类似的方式针对特定情况(具有潜在的大量性能增益)完成,尽管这通常在用于分配池块的operator new术语。

答案 9 :(得分:1)

我没有看到分配器如何工作而不跟踪其分配的大小。如果它没有这样做,它将如何知道哪些内存可用于满足未来的malloc请求?它必须至少存储某种包含地址和长度的数据结构,以指示可用内存块的位置。 (当然,存储空闲空间列表相当于存储已分配空格的列表)。

答案 10 :(得分:0)

嗯,你唯一需要的是一个指针,用来释放你之前分配的内存。字节数是由操作系统管理的,因此您不必担心它。没有必要获得free()返回的分配字节数。我建议你手动计算正在运行的程序分配的字节数/位置数:

如果您在Linux中工作并且想知道malloc已分配的字节/位置的数量,您可以创建一个使用malloc一次或n次的简单程序并打印出您获得的指针。此外,您必须让程序休眠几秒钟(足以让您执行以下操作)。之后,运行该程序,查找其PID,写入cd / proc / process_PID,然后输入“cat maps”。输出将在一个特定行中显示堆内存区域的开始和最终内存地址(在其中以dinamically方式分配内存)。如果打印出指向这些内存区域的指针,则可以猜出你分配了多少内存。

希望它有所帮助!

答案 11 :(得分:0)

为什么要这样? malloc()和free()是故意非常简单的内存管理原语,C中的高级内存管理主要取决于开发人员。 Ť

此外realloc()已经这样做了 - 如果减少realloc()中的分配,它将不会移动数据,并且返回的指针将与原始指针相同。

整个标准库通常都是由简单的原语组成,您可以从中构建更复杂的函数以满足您的应用需求。所以对于表格的任何问题的答案&#34;为什么标准库不做X&#34;是因为它无法完成程序员可能想到的所有事情(这是程序员的目的),所以它选择做的很少 - 构建自己的或使用第三方库。如果你想要一个更广泛的标准库 - 包括更灵活的内存管理,那么C ++可能就是答案。

你标记了C ++和C的问题,如果你正在使用C ++,那么你几乎不应该使用malloc / free - 除了new / delete,STL容器类自动管理内存,并且一种可能特别适合各种容器性质的方式。