C 中静态内存分配与动态内存分配的成本

Cost of static memory allocation vs dynamic memory allocation in C

当你知道 objects/items 的确切数量时,我很想知道什么是内存分配的首选方法 static vs dynamic 对性能有好处(例如,运行 时间)在 CLinux。少量对象(少量内存)和大量对象(大量内存)的成本。

e.g., type A[N] 对比 type *A = malloc(sizeof(type) * N)

请告诉我。谢谢。

注意:我们可以对此进行基准测试,并可能知道答案。但是我想知道解释这两种分配方法之间性能差异的概念。

静态分配会快得多。静态分配可以发生在全局范围内,也可以发生在堆栈上。

在全局范围内,静态分配的内存内置于二进制映像中。这是所需内存的总大小,它在 运行 二进制文件中的位置是在编译时计算的。然后当程序加载时,操作系统加载器将为所有全局静态数组分配足够的内存。我很确定它会在所有分配的固定时间内发生。 (例如,更多分配不会花费更多时间)

在本地范围内,静态分配是在堆栈上分配的。这涉及简单地在堆栈上保留固定数量的字节,并且每次分配都在恒定时间内发生。堆栈 space 非常有限。

动态内存必须从堆中分配,即使在最好的情况下,大多数分配所花费的时间也会随着每次分配的线性增加而增加,例如 n log n 时间之类的。

而且实际上动态分配比静态分配慢很多倍。

@update:正如下面的评论所指出的:堆栈分配在技术上不是静态分配(但它们是具有 OP 问题中使用的句法形式的分配)。

此外,在谈到 "allocation time" 时,我正在考虑管理内存(分配和释放)的总时间。

在一些动态分配器中,分配时间比释放时间快。

一些快速分配器以内存效率换取分配速度也是事实。在这些情况下,静态仍然是 "better",因为在分配精确大小的块时,静态和堆栈分配分别没有时间和常数时间。

动态分配器要快速权衡显着的内存效率(例如,伙伴分配器四舍五入到两个大小块的下一个幂,例如 33k 分配将使用 64k)

全局内存通常在您的程序(或共享 module/dll)加载并预填充其初始值时分配。这通常有自己的内存部分。

堆栈是内核在创建新线程时分配的内存块(一系列内存页)。在堆栈上分配堆栈内存通常是通过减少堆栈指针来完成的(一条机器指令 - (堆栈通常是完整的降序 - 较新的分配具有较低的地址)。删除堆栈对象时,堆栈指针会增加)。因此堆栈具有严格的先进后出语义。堆栈也是固定大小的,所以当你 运行 出来时,你会得到堆栈溢出。

当使用 malloc(在 C 中)或 new(C++)时,内存分配在 heap/free-store 上。这是任意数量的内存块。当需要的内存多于已分配的内存时,malloc 会向内核请求更多的内存。通常 malloc 将使用已从系统获得的释放内存。

必须假定对 malloc 的调用很慢,对于任何性能关键或实时例程都应避免调用。这是有两个原因。

  1. 堆需要以任意顺序分配和释放任意大小的内存。较旧的算法用于遍历释放块列表,直到找到合适大小的块。由于内存可能高度碎片化,因此这可能会很慢。如果堆在多个线程上使用,则需要提供某种锁定。已经进行了大量研究来改善这种情况,现代堆 jemalloc 和 tcmalloc 确实改善了一些事情。但是,不要指望这个。
  2. 如果无法从已由堆分配器管理的内存中分配所需的内存,则堆分配器将需要向内核请求更多内存。 (在 unix 上,这是通过 mmapsbrk 系统调用完成的)。内核需要找到一些未分配的内存页面,并且必须将这些页面映射到您的进程内存中 space)。任何形式的缓存都会超出 window)。所以预计这会非常慢(对于计算机)。

因此,如果您需要执行任何性能关键处理,请预先分配所有内存,然后重用已分配的内存。始终假设调用 malloc 和 free 会很慢。

如果您确切知道需要预留多少 space and,那么您最关心的是内存管理方面的性能 and 您的代码不需要可重入,那么您将需要分配具有 静态存储持续时间 的对象,方法是在文件范围内声明它们或使用 static 存储 class 说明符。

int data[SOME_NUMBER];

void foo( /* some list of parameters here */ )
{
  static int some_more_data[SOME_OTHER_NUMBER];
  ...
}

datasome_more_data都存在于程序的生命周期中,但是some_more_data只在foo函数中可见1

实际上,具有静态存储持续时间的项目 space 在二进制映像本身中为它们预留了空间,以便在加载程序后立即使用内存。这可能就地方性而言有优势;如果数据和代码在同一页中,则无需交换。当然,这取决于代码。

明显的缺点是您的可执行文件将占用更大的空间。另一个缺点是您的代码不是 re-entrantfoo 的每次调用在读取或写入 some_more_data 时都在同一内存块上工作。根据您的代码的作用,这可能是也可能不是什么大问题。

如果您的代码需要可重入,则不能使用具有静态存储持续时间的对象。如果数据块相对较小,请使用具有自动存储持续时间的对象(即常规局部变量)。

void foo( /* some list of parameters here */ )
{
  int some_more_data[SOME_SMALLISH_NUMBER];
  ...
}

在这种情况下,some_more_data 仅在 foo 函数的生命周期内存在;每次调用 foo 时,它都会自动分配另一个 some_more_data 对象。保留内存的任何开销都是首先调用函数的开销的一部分(至少根据我的经验)。无论您是否使用局部变量,堆栈指针仍会得到调整,因此使用局部变量不会使其变慢。主要问题是自动对象的可用内存相对较小;对于超过一定尺寸的物体,这种方法根本行不通。

如果您的代码需要可重入 并且 您需要分配大块内存,那么您几乎只能使用动态内存管理 (malloc/calloc/reallocfree)。根据您设计代码的方式,您可以最大限度地减少一些性能问题。


1。在从源代码到机器代码的翻译过程中强制执行可见性规则;他们在 运行 时间并不真正适用。

真正的静态分配(全局变量和标记为 static 的局部变量)被粘合到部分中并在加载时与部分的其余部分一起加载 (execve)使用 mmap。 它们基本上是免费的,已经包含在加载程序的费用中。

具有静态已知大小的自动数组可以与其他局部变量粘合在一起,并通过调整堆栈指针进行分配。 这本质上是一个整数减法(堆栈向下增长)或者在现代处理器上接近 1 ns

可变长度数组不能粘附到其他变量,因此它们每个的成本约为 1 ns

我尝试在单线程进程中测量不同大小的 mallocs,我得到了这个,这意味着 malloc+free 对的成本约为 50-100 分配堆栈变量的倍数 <16MiB。之后,它向上飙升(32MiB64MiB 的成本都是分配 <=16MiB 的大约一百倍):

不过,这些只是获取指针的成本。实际访问可能会导致页面错误,从而进一步增加内存块的成本。页面错误在堆栈分配中应该极为罕见,但在 first-time 访问动态分配的内存时很可能出现。

此外,许多小的动态分配会给缓存带来相当大的压力,因为您的内存会变得碎片化。 (您可以通过使用您自己的内存池或作为 glibc 的一部分提供的 obstack 功能来减少这种碎片。)