19

随着最近 C 得到的所有噪音,我读到有一些方法可以最大限度地减少 C 中 malloc 的使用,这是一个非常好的实践。我怎么也不知道这种做法何时、如何或什么是好的。所以我的问题是,也许一些有经验的 C 程序员可以举一些例子,其中一个人可以(或应该)在没有 malloc 的情况下编写一些东西,但是对于新手 C 程序员来说,什么是真正不明显的方式(因此说新手只会使用 malloc) ? 也许你有一些将 malloc 分解成其他东西的经验。

PS我读过的一些帖子引用了Quake 3源代码以及它如何避免使用malloc,所以如果有人知道这一点,知道那里做了什么会很有趣,因为至少知道我想避免深入研究地震漫无目的地编码。(因为如果他们避免使用 malloc 搜索 malloc 不会给出太多结果,我想,代码库也很可能不像单个示例那样简单)

4

6 回答 6

13

我不知道完全避免malloc,但你当然可以减少它。

基本概念是内存池。这是您分配的一个大缓冲区,您可以将其用于许多对象,而不是请求大量的小分配。

您可能会在实际情况中使用它,您将事件发送到队列中以由另一个线程处理。事件对象可能是小型结构,您确实需要避免malloc每秒进行数千次调用。

答案当然是从池中提取这些事件对象。如果需要,您甚至可以使用部分池缓冲区来形成一个列表,以便您可以快速索引已返回池的内存。这些通常被称为free-lists

您必须小心内存对齐,因为您可能会因数据未对齐而严重影响性能。但是你可以用一点数学来处理所有这些。


不要害怕这些概念。游泳池实际上不必那么复杂。考虑一下:

int ** matrix = malloc( rows * sizeof(int*) );
for( int i = 0; i < rows; i++ ) {
    matrix[i] = malloc( cols * sizeof(int) );
}

我经常看到这种情况,这是我的一个小烦恼。当你可以这样做时,你为什么要这样做:

int ** matrix = malloc( rows * sizeof(int*) );
matrix[0] = malloc( rows * cols * sizeof(int) );
for( int i = 1; i < rows; i++ ) {
    matrix[i] = matrix[i-1] + cols;
}

当然,这减少了这一点(但要注意第一行中潜在的对齐问题 - 为了清楚起见,我在这里忽略了它)

int ** matrix = malloc( rows * sizeof(int*) + rows * cols * sizeof(int) );
matrix[0] = (int*)matrix + rows;
for( int i = 1; i < rows; i++ ) {
    matrix[i] = matrix[i-1] + cols;
}

最后一个例子的酷点是删除矩阵是多么容易=)

free( matrix );

哦,对矩阵进行归零同样简单......

memset( matrix[0], 0, rows * cols * sizeof(int) );
于 2013-01-22T13:11:19.933 回答
5

在您需要本地范围内的小型动态大小数组的场景中,有 alloca()一个从堆栈中分配并且不需要您显式释放内存(它在函数返回时被释放),并且有可变长度数组( VLA) :

void meh(int s) {
    float *foo = alloca(s * sizeof(float));
    float frob[s];
} // note: foo and frob are freed upon returning
于 2013-01-22T13:01:50.833 回答
3

在某些特定情况下不使用的主要原因malloc可能是它采用了通用的、一刀切的内存分配方法。

在具有众所周知的分配需求的情况下,其他方法(例如内存池和平板分配)可能会带来好处。

例如,对于分配器来说,假设分配的对象将具有固定大小,或者假设它们的生命周期相对较短,则更为有利。通用分配器无法做出这样的假设,因此无法在这样的场景中发挥最佳性能。

潜在的好处可以包括减少内存占用,因为专用分配器具有更精简的簿记。通用分配器很可能为每个分配的对象保存大量元数据,而提前“知道”对象大小的分配器可能会从元数据中省略它。

它还可以在分配速度方面产生影响——自定义分配器可能能够更快地找到空槽。

这都是在亲戚那里谈论的,但是在选择自定义分配方案之前您应该问的问题是:

  • 您是否需要分配和释放大量具有相同大小的对象?(板坯分配)

  • 这些对象可以在没有单独调用开销的情况下立即处理吗?(内存池)

  • 是否有单独分配的对象的逻辑分组?(缓存感知分配)

底线是,您必须仔细检查程序的分配需求和模式,然后决定自定义分配方案是否有益。

于 2013-01-22T13:05:06.940 回答
3

如果您事先知道数组、列表、堆栈、树以及程序需要的任何数据结构的所有大小,则可以通过定义元素数量恒定的数组来静态分配所需的内存。优点:没有内存管理,没有内存碎片,速度快。缺点:使用有限,浪费内存。

您可以在操作系统提供的任何内容之上实现自定义内存分配器malloc(),一次分配一大块内存,然后在不调用标准malloc()函数的情况下将其分割。优点:快。缺点:实施正确并不容易。

另一种(也是相当反常的)避免malloc()方法是将大部分数据存储在文件中而不是内存中。优点:几乎没有。

如果您确定程序的堆栈足够大,您还可以使用局部变量和深度函数调用(或显式递归)为移动中的数据分配空间。优点:没有内存管理,简单,快速。缺点:使用有限。

作为一个避免工作的中型项目的示例,malloc()我可以提供我的宠物项目Smaller C compiler。它静态分配了许多数组,并且还在递归函数中分配了小的局部变量。请注意,代码还没有被美化,如果你是编程、C 或编译器的新手,这不是什么小东西或容易理解的东西。

于 2013-01-22T13:27:04.513 回答
2

有几个需要避免的原因malloc- 在我看来,最大的一个原因是“没有 malloc,没有 free”来解释 Bob Marley ......所以,不会因为“忘记”调用而导致内存泄漏free

当然,您应该始终检查NULL动态分配内存的时间。避免这种情况将减少代码量和代码的复杂性。

不幸的是,用完堆栈或全局变量大小的替代方案通常更糟糕,因为它要么立即崩溃而没有给用户任何有意义的错误消息(stackoverflow),要么全局变量中的缓冲区溢出 - 检查全局变量中的边界将避免这种情况,但是如果你检测到它,你会怎么做?没有太多选择。

另一部分当然是malloc与局部变量相比,调用可能会非常昂贵。当您在“热路径”中点击 malloc/free 调用时尤其如此 - 经常调用的部分代码。在小内存部分上使用也会产生内存开销malloc——过去在 Visual Studio 中的开销约为 32 字节的“标题”并四舍五入到 16 或 32 字节的边界——因此 1 字节的分配实际上占用了 64 字节。分配 17 个字节也将占用 64 个字节...

当然,就像所有工程/软件设计一样,它不是“您不得使用 malloc”,而是“如果有简单/合适的替代方案,请避免使用 malloc”。使用比它们需要的数倍大的所有全局变量是错误的,只是为了避免 malloc - 但是为图形绘制循环的每一帧或每个对象调用 malloc/free 也是同样错误的。

我没有看过 Quake 的代码,但我在 3DMark 2000 中编写了一些代码[我认为我的名字仍然在产品的学分中]。这是用 C++ 编写的,但它避免在渲染代码中使用 new/delete。这一切都是在框架的设置/拆卸中完成的,很少有例外。

于 2013-01-22T13:22:22.547 回答
2

分配更大的内存块通常更快,所以我的建议是分配一个大块,然后从中创建一个内存池。实现您自己的函数以将内存“释放”回池并从中分配内存。

于 2013-01-22T13:04:53.337 回答