内存分配/释放瓶颈?

52

在典型的现实世界程序中,内存分配/释放有多大瓶颈?欢迎回答任何需要考虑性能的程序类型。像malloc/free/垃圾收集等良好实现是否足够快,以至于只有在少数情况下才会成为瓶颈,或者大多数性能关键软件从尽量减少内存分配量或拥有更快的malloc/free/垃圾收集实现中获益显著?

注意:这里不涉及实时内容。通过输出吞吐量来衡量性能的东西,但延迟不一定重要。

编辑:尽管提到了malloc,但本问题并不是特定于C/C++。

12个回答

43

随着内存碎片化的增加,分配器需要在更大的堆中更努力地寻找您请求的连续区域,这一点尤为重要。大多数性能敏感的应用程序通常会编写自己的固定大小块分配器(例如,它们一次请求16MB的内存,然后将其分割成4kb、16kb等固定块),以避免这个问题。

在游戏中,我曾经看到过对malloc()/free()的调用消耗了高达15%的CPU资源(在编写不良的产品中),或者通过精心编写和优化的块分配器,只需5%的CPU资源。考虑到游戏必须保持一致的六十赫兹吞吐量,在垃圾回收运行时偶尔出现500毫秒停顿是不可行的。


4
我会尽力进行翻译并使其更通俗易懂,但不能改变原意。以下是需要翻译的内容:+1 - 我甚至会强调说:对于长时间运行的应用程序,碎片化是最大的分配问题。 - peterchen
1
“长时间运行”和“堆”并不是堆性能的好指标。像充分利用CPU缓存一样,技术才是关键。我的金融模拟运行了约8小时,但对象在调用树的高处分配,因此被分配一次后使用了数十亿次。99%的内存来自堆。微软曾经支持单个进程的多个堆(可能仍然如此),因此树和链表可以分配自己的大小并避免否则会导致的碎片化。同样,保持每个堆的分配为某些基本单位大小的倍数也有帮助。这两个原则非常有帮助。 - user2548100
1
堆栈使用更多关注对象的生命周期而非性能。在良好构建的程序中,性能是相同的。当您退出作用域时,堆栈分配确实使清理变得容易。_alloca()是从堆栈动态内存分配的好方法,但除了方便清理和可能防止碎片化外,与malloc()相比没有任何优势。https://caligari.dartmouth.edu/doc/ibmcxx/en_US/doc/libref/concepts/cumemmng.htm - user2548100

24

现在几乎所有的高性能应用程序都必须使用线程来利用并行计算。当编写C/C++应用程序时,这就是真正的内存分配速度杀手。

在C或C++应用程序中,malloc/new必须为每个操作锁定全局堆。即使没有争用,锁也远非免费,应该尽可能避免使用。

Java和C#在这方面做得更好,因为线程从一开始就被设计进去,并且内存分配器从每个线程池中工作。这也可以在C/C++中实现,但不是自动的。


+1,但对于C#来说是否正确呢?在关于内存分配和C#垃圾回收器的任何描述中,我都没有发现有关每个线程内存池的通知。此外,如果在不同的线程中释放内存,则它们将更加昂贵。 - peterchen
3
@peterchen:请参阅http://msdn.microsoft.com/en-us/magazine/bb985011.aspx,“在多处理器系统上,托管堆的第0代被分成多个内存区域,每个线程使用一个区域。这允许多个线程同时进行分配,从而不需要独占访问堆。” - Zan Lynx
3
现代的内存分配器如 tcmalloc 和 Hoard 会自动使用每个线程的堆来满足大多数分配请求。也就是说,在常见情况下,它们不使用锁,这使得它们快速且可扩展。 - EmeryBerger
2
@EmeryBerger:2010年,我在多线程测试中发现默认的Microsoft和GNU C库表现不佳。这就是为什么严肃的项目似乎使用第三方malloc库的原因。也许自那时以来默认库已经得到改进。我有一段时间没有进行过严格的测试了。 - Zan Lynx

11

首先,由于您提到了malloc,我假设您是在谈论C或C++。

内存分配和释放往往是现实世界程序的一个重大瓶颈。当您分配或释放内存时,“底层”会发生很多事情,所有这些都是系统特定的;内存实际上可能被移动或碎片化,页面可能会重新组织 - 没有平台无关的方式来知道影响将是什么。某些系统(如许多游戏机)也不进行内存碎片整理,因此在这些系统上,随着内存变得分散,您将开始出现内存不足错误。

一个典型的解决方法是尽可能预先分配大量内存,并保留它直到程序退出。您可以使用该内存来存储大型单块数据集,或者使用内存池实现来分配它。许多C/C++标准库实现出于这个原因会自己执行一定数量的内存池操作。

但是毫无疑问,如果您有一个时间敏感的C/C++程序,并且要执行大量的内存分配/释放,则会降低性能。


1
C或C++系统如何进行内存碎片整理?在我看来,内存整理意味着以前由malloc()返回的指针变得过时,必须进行更新。据我所知,在这些语言中是不可能实现的。 - unwind
2
抱歉如果我表达不清楚——我的意思是操作系统可以进行碎片整理。在使用分页的操作系统中,内存可以在页面之间移动,并且内存位置被重新映射到不同的页面。 - MattK

7
总的来说,在大多数应用程序中,内存分配的成本可能被锁争用、算法复杂度或其他性能问题所掩盖。一般情况下,我认为这可能不是我担心的前十大性能问题之一。
现在,获取非常大的内存块可能会成为一个问题。而获取但未正确释放内存则是我担心的问题。
在Java和基于JVM的语言中,新建对象现在非常非常快速。
这里有一篇由一个知识渊博的人撰写的不错文章,底部附有更多相关链接: http://www.ibm.com/developerworks/java/library/j-jtp09275.html

4
在Java中(以及其他具有良好GC实现的语言中),分配对象非常便宜。在SUN JVM中,它仅需要10个CPU周期。在C / c ++中进行malloc要昂贵得多,因为它需要做更多的工作。
尽管在Java中分配对象非常便宜,但同时为大量Web应用程序用户分配对象仍可能导致性能问题,因为这将触发更多垃圾回收器运行。 因此,在Java中分配时会产生间接成本,由GC执行的释放引起。这些成本很难量化,因为它们非常取决于您的设置(您拥有多少内存)和您的应用程序。

3
如果分配只需10个周期,那么它就不会执行任何搜索,必须附加到已分配内存的末尾。缺点是在垃圾回收后紧缩内存以消除空隙。如果您正在进行大量的new/delete操作,这种方法的性能将很差。 - Skizz
不会的。JVM会一次性分配和释放大块内存,而单个的new/delete操作只是从预先分配的内存池中获取和释放内存。这个过程非常便宜。 - skaffman
原因在于SUN JVM(到目前为止)使用拷贝分配器来进行新空间的分配。有一个“to”空间和一个“from”空间,其中一个始终是完全空闲的。 - kohlerm
2
是的Skizz,你说得对。这种廉价分配在压缩方面会反弹。在Java中,它可能会影响性能,整个垃圾收集和碎片整理调优和破解是Java中的大问题。这就是为什么我们有新的Collector参数和新的收集器用于新的Java机器。随着新实现的出现,我们可以使用StringBuilder等工具来消除制作新对象的需要。 - bartosz.r

4
一种Java虚拟机可以独立于应用程序代码的执行而从操作系统中获取和释放内存。这使它能够以大块方式获取和释放内存,这比手动内存管理的小规模单个操作效率要高得多。 这篇文章是2005年写的,JVM风格的内存管理已经遥遥领先。自那时以来,情况只有得到改善。
哪种语言拥有更快的原始分配性能,Java语言还是C/C++?答案可能会让您感到惊讶-现代JVM中的分配速度比最佳表现的malloc实现要快得多。在HotSpot 1.4.2及更高版本中,new Object()的常见代码路径大约为10条机器指令(Sun提供的数据;请参见资源),而C中表现最佳的malloc实现平均需要每次调用60到100条指令(Detlefs等人提供的数据;请参见资源)。分配性能不是整体性能的一个微不足道的组成部分-基准测试显示,许多真实世界的C和C ++程序(如Perl和Ghostscript)将其总执行时间的20%至30%花费在malloc和free上-远高于健康的Java应用程序的分配和垃圾收集开销。

3

在性能方面,分配和释放内存是相对昂贵的操作。现代操作系统中的调用必须全部经过内核,以便操作系统能够处理虚拟内存、分页/映射、执行保护等。

另一方面,几乎所有现代编程语言都将这些操作隐藏在“分配器”后面,这些分配器使用预先分配的缓冲区。

大多数关注吞吐量的应用程序也使用这个概念。


3

我知道之前已经回答了问题,但那只是对另一篇答案的回应,并非针对你的问题。

直接与你交流,如果我理解正确的话,你的性能使用情况的标准是吞吐量。

在我看来,这意味着你应该几乎完全关注NUMA aware allocators

早期的参考文献:IBM JVM论文、Microquill C、SUN JVM都没有涉及此点,因此我高度怀疑它们在今天的应用中,至少在AMD ABI方面,NUMA是卓越的内存-CPU管理者。

无论是现实世界、虚拟世界还是其他世界,NUMA感知内存请求/使用技术都更快。不幸的是,我目前正在运行Windows,而我还没有找到在Linux中可用的"numastat"。

我的一个朋友在FreeBSD内核的实现中深入写过这个问题。

尽管我能够展示出现场节点内存请求数量通常非常大,而且优势显而易见(突出了明显的性能吞吐量),但你可以肯定地进行基准测试,因为你的性能特征将是高度特定的。

我知道,在很多方面,至少早期的5.x VMWARE表现相当差,因为它没有利用NUMA,经常从远程节点要求页面。然而,VM是一种非常独特的内存分区或容器化技术。

我引用的参考文献之一是Microsoft针对AMD ABI的API实现,其中有专门为用户应用程序开发人员设计的NUMA分配专用接口;)

这里是一些浏览器插件开发人员最近的分析,包括可视化对比了4种不同的堆实现。自然而然地,他们开发的那个排名第一(有趣的是测试者通常表现出最高的分数)。
他们在某些方面量化地探讨了空间/时间的确切权衡,在他们的用例中至少已经确定了LFH(哦,对了,顺便说一下,LFH显然只是标准堆的一种模式)或类似设计的方法从一开始就会消耗更多的内存,但随着时间的推移,可能会使用更少的内存...图形也很不错...
我认为,根据您的典型工作负载选择堆实现是一个好主意,但在优化这些细节之前,请确保您的基本操作是正确的,并充分了解您的需求。

3

这是c/c++内存分配系统最擅长的领域。默认的分配策略对大多数情况都可以使用,但可以根据需要进行更改。在GC系统中,你无法做太多事情来改变分配策略。当然,这也有代价,那就是需要跟踪分配并正确释放它们。C++更进一步,可以使用new运算符为每个类指定分配策略:

class AClass
{
public:
  void *operator new (size_t size); // this will be called whenever there's a new AClass
   void *operator new [] (size_t size); // this will be called whenever there's a new AClass []
  void operator delete (void *memory); // if you define new, you really need to define delete as well
  void operator delete [] (void *memory);define delete as well
};

许多STL模板也允许您定义自定义分配器。
与所有优化相关的事情一样,您必须首先通过运行时分析确定内存分配是否真的是瓶颈,然后才编写自己的分配器。

GC系统并非完全如此。一些Java虚拟机具有足够的内存配置选项,足以让人眼花缭乱。不过,祝你好运找出要使用的选项。 - Zan Lynx

2
根据MicroQuill SmartHeap Technical Specification,“一个典型的应用程序[...]将其总执行时间的40%花费在内存管理上”。您可以将此数字视为上限,我个人认为典型应用程序花费10-15%的执行时间来分配/释放内存。在单线程应用程序中很少成为瓶颈。
在多线程C / C ++应用程序中,由于锁争用,标准分配器成为问题。这是您开始寻找更可扩展解决方案的地方。但请记住阿姆达尔定律

1
40% 很可能是为了帮助他们销售产品而做出的虚假宣称。我的猜测是 5-20% 是覆盖了95% 应用程序的范围。 - Suma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接