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

Posted

技术标签:

【中文标题】C 中静态内存分配与动态内存分配的成本【英文标题】:Cost of static memory allocation vs dynamic memory allocation in C 【发布时间】:2015-07-23 20:25:02 【问题描述】:

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

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

请告诉我。谢谢。

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

【问题讨论】:

这是两种完全不同的“成本”。就执行时间而言,静态分配是“免费的”,而如果使用不当,则会消耗内存。动态在内存使用方面是最佳的(同样,如果使用得当),但会花费一些处理器时间开销。 静态分配的大小限制也比动态分配低得多。 确实应该没什么区别。内存必须以任何一种方式分配,这只是操作系统链接器/加载器执行它还是您的程序执行它的问题。如果它可以由加载器完成,那么根据定义,它是一个外部循环成本,并且完全不相关。 非常感谢您的回答。请为我的问题投票,因为我需要一些分数来为答案投票。 @samarasa,无论您的代表如何,如果您觉得满意,请接受答案。 (点击答案旁边的复选标记即可。) 【参考方案1】:

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

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

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

动态内存必须从堆中分配,即使在最好的情况下,大多数分配所花费的时间也会随着每次分配而超过线性扩展,例如 n log n 时间或其他东西。

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

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

另外,当谈到“分配时间”时,我正在考虑管理内存的总时间(alloc 和 free)。

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

有些快速分配器以内存效率换取分配速度也是事实。在这些情况下,静态仍然“更好”,因为在分配精确大小的块时,静态和堆栈分配分别不是时间和恒定时间。

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

【讨论】:

非常感谢您的回答。请为我的问题投票,因为我需要一些分数来为答案投票。 堆内存分配算法的复杂度变化很大。诸如首次拟合/最佳拟合之类的常见算法通常是线性的,除非有一些巧妙的索引数据结构。二进制伙伴可以是恒定的分配时间和对数的释放 - 以及基于某些预定义大小(如 2 的幂)的空闲列表的简单隔离存储/平板分配器 我真的不认为你应该说堆栈空间是静态分配的。它根据需要分配然后释放 - 这不是静态的 虽然堆分配可能昂贵(通常释放比分配更昂贵),但堆分配的开销通常被高估。当你打开灯时,它就是你床下的那些怪物之一。当我对一个每秒只能交付 18-20 百万的无锁生产者-消费者队列进行基准测试时,我感到很失望,而我预计会是 2-3 倍。直到我的同事问我:“呃,你确实意识到你也在每秒进行 2000 万次堆分配?”。现在,您每秒可以执行 2000 万次的任何事情都不会令人担忧。 @Rafael Baptista 有一件事,我想从你这里知道......“它位于正在运行的二进制文件中,是在编译时计算的。”这意味着编译器对内存的位置有一个想法(对于变量、数组等)。但是,如果我在另一台 PC 上运行编译器在一台 PC 中生成的 exe 文件。那么根据你的说法,编译器也会对另一台PC的内存分配位置有所了解?【参考方案2】:

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

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

当使用 malloc (C) 或 new (C++) 时,内存分配在堆/空闲存储区。这是任意数量的内存块。当需要的内存多于已分配的内存时,malloc 会转到内核向系统请求更多内存。通常 malloc 将使用已从系统获得的释放内存。

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

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

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

【讨论】:

【参考方案3】:

如果您确切知道需要留出多少空间并且您主要关心的是内存管理方面的性能并且您的代码不需要重新-entrant,那么您将希望通过在文件范围内声明对象或使用static 存储类说明符来分配具有静态存储持续时间的对象。

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

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

明显的缺点是您的可执行文件会占用更大的空间。另一个缺点是您的代码不是re-entrant; foo 的每次调用在读取或写入 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。在从源代码到机器代码的转换过程中强制执行可见性规则;它们在运行时并不真正适用。

【讨论】:

静态内存和堆栈内存(用于主线程)都是在加载时分配的。按照同样的逻辑,在执行阶段早期分配大块内存应该不会出现问题。然而,不断使用 malloc 和 free 可能会出现性能瓶颈。 @doron:但是在函数进入之前,不会为堆栈上的特定 object 留出内存。没错,这只是调整堆栈指针的问题,无论如何这都是函数调用开销的一部分。就像我说的,使用 auto 变量不会让事情变得更慢。【参考方案4】:

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

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

可变长度数组不能与其他变量绑定,因此每个变量的成本约为 1 ns

我尝试在单个线程进程中测量不同大小的 mallocs,我得到了这个结果,这意味着 malloc+free 对的成本大约是堆栈变量的 50-100 倍分配 。之后,它会飙升(32MiB64MiB 的成本都是分配

这些只是获取指针的成本。实际访问可能会导致页面错误,从而进一步增加内存块的成本。页面错误在堆栈分配中应该非常罕见,但在首次访问动态分配的内存时可能会发生。

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

【讨论】:

以上是关于C 中静态内存分配与动态内存分配的成本的主要内容,如果未能解决你的问题,请参考以下文章

动态内存分配与静态内存分配

C与C++申请动态内存空间的异同

Java静态内存与动态内存分配的解析

C之静态内存和动态内存

系统C/C++内存管理之内存分配

c语言中啥是动态分配内存?