我们为什么要使用内存管理器

Why we use memory managers?

本文关键字:内存 管理器 为什么 我们      更新时间:2023-10-16

我看到很多代码库,特别是服务器代码都有基本的(有时是高级的)内存管理器。内存管理器的真正目的是减少malloc调用的数量,还是主要用于内存分析、损坏检查或其他以应用程序为中心的目的。

保存malloc调用的参数是否足够合理,因为malloc本身就是一个内存管理器。我能想到的唯一性能提升是当我们知道系统总是要求相同大小的内存时。

或者使用内存管理器的原因是free不会将内存返回给操作系统,而是保存在列表中。因此,在进程的整个生命周期中,如果我们因为碎片而继续执行malloc/free,那么进程的堆使用可能会增加。

malloc是一个通用的分配器- "不慢""总是快"更重要。

考虑一个特性,它在许多常见情况下可以提高10%,但在少数罕见情况下可能会导致显著的性能下降。特定于应用程序的分配器可以避免这种罕见的情况并从中获益。通用分配器不应该这样做。


除了调用malloc的次数,还有其他相关属性:

分配位置
在当前的硬件上,这很容易成为影响性能的最重要因素。应用程序对访问模式有更多的了解,可以相应地优化分配。

多线程
一个通用的分配器必须允许调用malloc和从不同的线程释放。这通常需要一个锁或类似的并发处理。如果堆非常繁忙,这将导致大量争用。

如果一个应用程序知道某些高频率的分配/释放只来自一个线程,那么它可以使用自己的线程专用堆,这不仅可以避免对这些分配的争用,还可以增加它们的局域性,并减轻默认分配器的负载。

分裂


对于在物理内存或地址空间有限的系统上长时间运行的应用程序来说,这仍然是一个问题。即使实际工作集没有增加,碎片也可能需要越来越多的操作系统内存或地址空间。对于需要不间断运行的应用程序来说,这是一个严重的问题。

上次我深入研究分配器(可能是五年前的事了)时,大家一致认为,减少碎片化的天真尝试常常与never slow规则相冲突。

同样,知道(它的一些)分配模式的应用程序可以从默认分配器中承担大量负载。一个非常常见的用例是构建语法树或类似的东西:有无数的小分配,它们永远不会单独释放,只能作为一个整体释放。这样的模式可以用一个非常简单的分配器有效地提供服务。

弹性和诊断
最后,默认分配器的诊断和自我保护功能可能不足以满足许多应用程序的需要。

为什么我们有自定义内存管理器而不是内置的?

第一个原因可能是代码库最初是在20-30年前编写的,当时提供的代码并不好,没有人敢改变它。

但是,正如你所说的,因为应用程序需要管理碎片,在启动时抓取内存以确保内存始终可用,出于安全或一堆其他原因-其中大部分可以通过正确使用内置管理器来实现。

C和c++是被设计为剥离的。它们不会做很多没有被明确要求的事情,所以当一个程序要求内存时,它会得到提供内存所需的尽可能小的努力。

换句话说,如果你不需要它,你就不用为它付钱。

如果需要更细粒度的内存控制,那是程序员的领域。如果程序员希望以裸机速度换取在目标硬件上提供更高性能的系统,并结合程序的通常独特的目标、更好的调试支持,或者只是喜欢使用管理器带来的外观和感觉以及温暖的模糊感,那么这取决于他们。程序员要么写一些更聪明的东西,要么找一个第三方库来做他们想做的事情。

您在问题中简要地谈到了使用内存管理器的许多不同原因。

内存管理器的真正目的是减少malloc调用的数量,还是主要用于内存分析、损坏检查或其他以应用程序为中心的目的?

这是个大问题。任何应用程序中的内存管理器都可以是通用的(如malloc),也可以是更具体的。内存管理器越专门化,它在完成特定任务时的效率就越高。

看这个过于简化的例子:

#define MAX_OBJECTS 1000
Foo globalObjects[MAX_OBJECTS];
int main(int argc, char ** argv)
{
  void * mallocObjects[MAX_OBJECTS] = {0};
  void * customObjects[MAX_OBJECTS] = {0};
  for(int i = 0; i < 1000; ++i)
  {
    mallocObjects[i] = malloc(sizeof(Foo));
    customObjects[i] = &globalObjects[i];
  }
}
在上面的代码中,我假设这个全局对象列表是我们的"自定义内存分配器"。这只是为了简化我所解释的内容。

当你用malloc分配时,不能保证它就在前一个分配的旁边。Malloc是一个通用的分配器,在这方面做得很好,但并不一定为每个应用程序做出最有效的选择。

使用自定义分配器,您可能能够预先为1000个自定义对象分配空间,并且由于它们是固定大小,因此返回您需要防止碎片并有效分配该块的确切内存量。

内存抽象和自定义内存分配器之间也存在差异。STL分配器可以说是一种抽象模型,而不是自定义内存分配器。

看看这个链接,了解更多关于自定义分配器的信息,以及它们为什么有用:gamedev.net link

我们想要这样做的原因有很多,这真的取决于应用程序本身。事实上,你提到的所有理由都是正确的。

我曾经构建了一个非常简单的内存管理器,它跟踪shared_ptr分配,以便我看到在应用程序端没有正确释放的内容。

我会说坚持你的运行时,除非你需要它不提供的东西。

内存管理器基本上用于有效地管理您的内存预留。通常情况下,进程只能访问有限的内存(32位系统中为4GB),您必须从中减去为内核保留的虚拟内存空间(取决于您的操作系统配置,为1GB或2GB)。因此,实际上进程可以访问3GB的内存,这些内存将用于保存它的所有段(代码、数据、bss、堆和堆栈)。

内存管理器(例如malloc)尝试通过向OS请求新的内存页(使用sbrk或mmap系统调用)来满足进程发出的不同内存保留请求。每次发生这种情况都意味着程序执行的额外成本,因为操作系统必须寻找合适的内存页分配给进程(物理内存是有限的,所有正在运行的进程都想使用它),更新进程表(TMP等)。这些操作非常耗时,并且会影响流程的执行和性能。因此,内存管理器通常会尝试请求所需的页面,以巧妙地完成进程保留。例如,它可以请求更多的页面,以避免在不久的将来调用更多的mmap调用。此外,它还尝试处理诸如碎片、内存对齐等问题。这基本上将进程从这个责任中解脱出来,否则每个编写需要动态内存分配的程序的人都必须手动执行此操作!

实际上,在某些情况下,您可能对手动执行内存管理感兴趣。这是嵌入式或高可用性系统必须全天候运行的情况。在这些情况下,即使内存碎片很低,在运行很长一段时间后(例如1年)也可能成为一个问题。因此,在这种情况下使用的解决方案之一是使用内存池预先为应用程序对象分配内存。此后,每次需要为某个对象使用内存时,只需使用已经预留的内存。

对于基于服务器或任何需要长时间或无限期运行的应用程序,主要问题是分页内存碎片。经过长时间的mallos/new和free/delete操作后,分页内存可能会在页面中出现空白,从而浪费空间,并最终耗尽虚拟地址空间。微软通过它的。net框架来处理这个问题,通过偶尔暂停一个进程来重新打包一个进程的分页内存。

为了避免在进程中重新打包内存时减速,服务器类型的应用程序可以为应用程序使用多个进程,以便在重新打包一个进程时,其他进程承担更多的负载。