我提出这个问题是为了确定哪种内存分配算法可以在性能关键的应用中(如游戏引擎或嵌入式应用程序)提供更好的结果。结果实际上取决于内存碎片化的百分比和内存请求的时间确定性。
教科书上有几种算法(例如伙伴内存分配),但也有其他算法,如TLSF。因此,就可用的内存分配算法而言,哪一种最快且导致的碎片最少。另外,垃圾收集器不应包括在内。
请注意,这个问题不涉及性能分析,只旨在找到满足给定要求的最佳算法。
我提出这个问题是为了确定哪种内存分配算法可以在性能关键的应用中(如游戏引擎或嵌入式应用程序)提供更好的结果。结果实际上取决于内存碎片化的百分比和内存请求的时间确定性。
教科书上有几种算法(例如伙伴内存分配),但也有其他算法,如TLSF。因此,就可用的内存分配算法而言,哪一种最快且导致的碎片最少。另外,垃圾收集器不应包括在内。
请注意,这个问题不涉及性能分析,只旨在找到满足给定要求的最佳算法。
一切都取决于应用程序。例如,可以在定义的时刻清除与特定请求相关的所有内存的服务器应用程序,其内存访问模式将与视频游戏等应用程序不同。
如果有一种内存分配算法总是最佳性能和最小碎片化,那么实现malloc
和new
的人员难道不会选择这种算法吗?
现在,通常最好假设编写操作系统和运行时库的人员并非白痴;除非您具有某些异常的内存访问模式,否则请勿试图超越他们。
相反,尝试减少您进行的分配(或重新分配)次数。例如,我经常使用std::vector
,但如果我事先知道它将有多少个元素,我可以一次性保留所有元素。这比通过多次调用push_back()
来让其自然增长要高效得多。
许多人来自那些new
只表示“给我一个对象”的语言,就会毫无理由地分配东西。如果您不必将其放在堆上,请勿调用new
。
至于碎片化:仍然取决于情况。不幸的是,我现在找不到链接,但我记得有一篇来自微软某个人员的博客文章,他曾经处理过一种C++服务器应用程序,该应用程序受到了内存碎片化的影响。该团队通过从两个区域分配内存来解决了这个问题。所有请求的内存都将来自区域A,直到它满为止(请求将像往常一样释放内存)。当区域A已满时,将从区域B分配所有内存。当区域B也满时,区域A再次完全为空。这解决了他们的碎片化问题。
这会解决你的问题吗?我不确定。你正在开展一个为多个独立请求提供服务的项目吗?是在开发游戏吗?
至于确定性:这仍然取决于情况。你的截止日期是什么时候?如果错过了截止日期会发生什么(宇航员在太空中迷失吗?播放的音乐开始听起来像垃圾?)?有实时分配器,但要记住:“实时”意味着“承诺满足截止日期”,不一定意味着“快速”。
我刚看到一篇文章,介绍了Facebook在jemalloc中加速和减少内存碎片的各种措施。您可能会发现该讨论很有趣。
Barış:
你的问题很笼统,但这是我的答案/指导:
我不知道游戏引擎,但对于嵌入式和实时应用程序,分配算法的一般目标是:
1-有界执行时间:您必须事先知道最坏情况下的分配时间,以便您可以相应地计划您的实时任务。
2-快速执行:显然,越快越好
3-始终分配:特别是对于实时的、安全关键的应用程序,所有请求都必须得到满足。如果您请求一些内存空间并得到一个空指针:麻烦!
4-减少碎片化:虽然这取决于所使用的算法,但通常,较少碎片化的分配提供更好的性能,原因包括缓存效应等多种因素。
在大多数关键系统中,您不能开始动态分配任何内存。您分析您的要求并确定您的最大内存使用量,并在应用程序启动时分配一个大块内存。如果您不能,则应用程序甚至不会启动,如果它确实启动了,在执行过程中不会分配任何新的内存块。
如果速度是一个问题,我建议采用类似的方法。您可以实现一个管理内存的内存池。该池可以在应用程序开始时初始化一个“足够”的内存块,并从该块中提供您的内存请求。如果您需要更多的内存,池可以进行另一个-可能很大的-分配(预期有更多的内存请求),并且您的应用程序可以开始使用这个新分配的内存。还有各种内存池方案,管理这些池也是另一个整体话题。
至于一些例子:VxWorks RTOS曾经采用一种首次适配分配算法,其中算法分析链表以找到足够大的空闲块。在VxWorks 6中,他们使用最佳匹配算法,其中空闲空间保留在树中,分配遍历树以找到足够大的空闲块。有一篇名为“Memory Allocation in VxWorks 6.0”的白皮书,作者是Zoltan Laszlo,您可以通过谷歌搜索找到更多细节。
回到你关于速度/碎片化的问题:这实际上取决于你的应用程序。要考虑的事情包括:
您将进行许多非常小的分配,还是相对较大的分配?
分配会突然出现,还是在应用程序中平均分布?
分配的生命周期是多长?
如果您之所以提出这个问题是因为您将实现自己的分配器,那么您应该设计它的方式,以便您可以更改底层的分配/释放算法,因为如果速度/碎片化对您的应用程序真的很关键,您会想尝试不同的分配器。如果我不知道您的任何要求就要推荐一些东西,我会从TLSF开始,因为它具有良好的整体特性。
char pool[MAX_MEMORY_REQUIRED_TO_RENDER_FRAME];
char *poolHead = pool;
void *alloc(size_t sz) { char *p = poolHead; poolHead += sz; return p; }
void free() { poolHead = pool; }
正如其他人所写的那样,每种应用没有“最佳算法”。已经证明,对于任何可能的算法,您都可以找到一种分配序列,它将导致碎片。
以下是我从游戏开发经验中提出的一些提示:
游戏开发领域的常见做法(在某种程度上仍然如此)是通过像瘟疫一样避免内存分配来解决动态内存分配性能问题。通常情况下可以使用基于堆栈的内存-即使对于动态数组,您通常也可以估计一个可以为您覆盖99%情况的边界,只有当您超过此边界时才需要分配。另一个常用的方法是“预分配”:估计您在某个函数或某个对象中需要多少内存,创建一种类似于小型简单的“本地堆”,从而您可以预先分配并从该堆中执行单个分配。
另一个选择是使用一些内存分配器库-它们通常由该领域的专家创建以满足某些特殊要求,如果您具有类似的要求,则它们可能符合您的要求。
有一种特殊情况,在这种情况下,您会发现“默认”操作系统/CRT分配器表现不佳,那就是多线程。如果您的目标是Windows,请注意Microsoft提供的操作系统和CRT分配器(包括否则出色的Low Fragmentation Heap)当前都是阻塞的。如果您想执行重要的线程操作,则需要尽可能减少分配或使用一些替代方案。请参见Can multithreading speed up memory allocation?
值得一提的一个限制,尚未提及的是多线程:标准分配器必须实现以支持多个线程,所有线程都可以同时分配/释放,并将对象从一个线程传递到另一个线程,以便由不同的线程进行释放。
正如您从该描述中猜测的那样,实现处理所有这些的分配器是一项棘手的任务。而且,这会损失性能,因为不可能在没有线程间通信(=使用原子变量和锁)的情况下满足所有这些约束条件,这是相当昂贵的。
因此,如果您可以避免在分配中使用并发,那么您就有很大机会实现自己的分配器,其性能显着优于标准分配器:我曾经自己做过这件事,并且用基于许多固定大小内存池的相对简单的分配器为小对象堆栈化空闲对象,使用了一个侵入式链接列表,每次分配可节省大约250个CPU周期。
当然,避免并发可能对您来说不可行,但如果您不使用它,利用这一事实可能值得考虑。