多线程能加速内存分配吗?

14

我正在使用一台8核处理器,并且正在使用Boost线程运行一个庞大的程序。 从逻辑上讲,该程序可以分成多个组,每个组由一个线程运行。 在每个组内,一些类总共会调用“new”运算符10000次。 Rational Quantify显示,“new”内存分配占用了程序运行时的最大处理时间,并减慢了整个程序的运行速度。

我可以通过在每个“组”内部使用线程来加快系统速度,使得这10000个内存分配可以并行进行。

我不清楚在这里内存分配将如何被管理。操作系统调度程序真的能够并行分配内存吗?


感谢您对应用程序进行性能分析。 - GManNickG
1
@所有人:好的,"堆争用"是在这方面寻找正确短语。显然,从glibc v2开始,可以并行处理malloc http://www.citi.umich.edu/projects/linux-scalability/reports/malloc.html,但与free()的争用只会从版本2.2.4开始处理http://www.bozemanpass.com/info/linux/malloc/Linux_Heap_Contention.html。我想知道这是否意味着像Hoard这样的库将变得多余。 - Nav
10个回答

12

标准CRT

在旧版的 Visual Studio 中,默认的 CRT 分配器是阻塞式的,但至少从 Visual Studio 2010 开始,它会直接调用相应的操作系统函数。Windows 堆管理器在 Windows XP 之前一直是阻塞的,在 XP 中,可选的低碎片堆不是阻塞的,而默认的堆则是阻塞的,而新版本的操作系统(如Vista / Win7)默认使用LFH。最近(Windows 7)分配器的性能非常好,与下面列出的可扩展替代品相当(如果针对较旧的平台或需要它们提供的某些其他功能,则仍可能更喜欢它们)。存在数个“可扩展分配器”,具有不同的许可证和不同的缺点。我认为,在Linux上,默认运行时库已经使用可扩展分配器(PTMalloc 的某些变体)。

可扩展替代品

我知道以下内容:

你可能想查看可伸缩内存分配器的使用经验,以获取我在尝试在Windows项目中使用它们时的经验。
实际上,它们中的大多数通过拥有每个线程缓存和每个线程预分配区域来工作进行内存分配,这意味着小的内存分配通常仅在线程上下文中发生,操作系统服务只会偶尔调用。

嘿,谢谢!只是要补充一下列表,Intel Threading Building Blocks 还有 scalable_malloc、scalable_free、scalable_realloc、scalable_calloc、scalable_allocator 和 cache_aligned_allocator。 - Nav
Suma,这也不正确。所有现代的MSVC版本默认使用操作系统堆函数(除非明确禁用)。如果启用了低碎片堆,则操作系统堆函数的性能将非常好,默认情况下自Windows Vista以来已启用(在Windows XP上,应用程序可以通过简单调用HeapSetInformation()启用它)。启用LFH后,Windows堆的性能可与最快的其他分配器相媲美-我个人对NedMalloc进行了基准测试,差异微不足道。 - Paul Groke
@PaulGroke 您是正确的,我已经尝试更新答案。 - Suma

7
动态分配内存使用应用程序/模块/进程的堆(但不是线程)。堆一次只能处理一个分配请求。如果您尝试在“并行”线程中分配内存,则堆会按顺序处理它们。您不会得到这样的行为:一个线程正在等待获取其内存,而另一个线程可以请求一些内存,而第三个线程正在获取一些内存。线程将必须排队等待以获取它们的内存块。
您需要的是堆池。使用任何此时未忙碌的堆来分配内存。但是,您必须在此变量的整个生命周期中注意,以使其不会在另一个堆上被释放(这会导致崩溃)。
我知道Win32 API有GetProcessHeap(),CreateHeap(),HeapAlloc()和HeapFree()等函数,允许您创建新堆并从特定堆HANDLE分配/释放内存。我不知道其他操作系统是否有类似的功能(我已经寻找了它们,但没有找到)。
当然,您应该尝试避免频繁进行动态分配。但是,如果您无法避免,您可以考虑(为了可移植性)创建自己的“堆”类(不必是堆本身,只需是非常高效的分配器),可以管理大块内存,以及一个智能指针类,它将持有对其来自的堆的引用。这将使您能够使用多个堆(确保它们是线程安全的)。

问题:您所说的堆池是指这个吗:http://en.wikipedia.org/wiki/Memory_pool?(我在想您是否在谈论内存池,那么我可以使用TBB可扩展分配器。但是像Scott Meyers这样的人已经对自定义分配器提出了质疑http://en.wikipedia.org/wiki/Allocator_%28C%2B%2B%29#Custom_allocators) - Nav
通过堆池,我只是指有一个堆列表供您使用(无论是操作系统原生堆,自制堆,还是boost之类的库),并且您从当前没有繁忙的堆中分配内存(即基于繁忙程度,可用内存和碎片化程度的优先级队列)。当然,除非您仔细并做得非常好,否则不建议使用自定义分配器。总的来说,我建议您选择其他人在这里建议的一些现成的东西(HOARD或TBB乍一看似乎相当可靠)。 - Mikael Persson
4
Mikael,你的说法不正确。现代堆实现使用线程缓存等技术来加速并行分配。这意味着你可以通过多个并发线程进行显着更多的分配,而不仅仅是一个线程。 - Paul Groke

5

我知道两种可扩展的malloc替代品:

我没有使用Hoard的经验(在研究中表现不佳),但Emery Berger在这个网站上潜伏,并对结果感到惊讶。他说他会看一下,我推测可能有一些特定的测试或实现导致Hoard被“困住”,因为一般反馈通常是好的。

关于jemalloc,需要注意的一点是,当您快速创建和丢弃线程时,它可能会浪费一些空间(因为它为您从每个线程分配的内存创建一个新池)。如果您的线程稳定,这应该不会有任何问题。


4
我相信对于你的问题的简短回答是:是的,很可能可以实现。正如这里已经有几个人指出的那样,有多种方法可以达到这个目标。
除了你的问题和这里已经发布的答案之外,最好从你的期望开始改进,因为这将基本上决定要采取哪条道路。也许你需要快100倍。此外,你是否认为自己在不久的将来也会进行速度改进,或者有一个足够好的水平?不知道你的应用程序或问题领域,也很难具体建议你。例如,你是否处于需要不断提高速度的问题领域?
当进行性能改进时,一个好的开始是质疑你是否需要按照当前的方式去做事情?在这种情况下,你能否预先分配对象?系统中是否有最大数量的X对象?你能重复使用对象吗?所有这些都更好,因为你不一定需要在关键路径上进行分配。例如,如果你可以重复使用对象,则具有预分配对象的自定义分配器将工作得很好。此外,你使用的操作系统是什么?
如果你没有具体的期望或一定的性能水平,只需尝试这里的任何建议,你就会发现更多信息。
祝你好运!

预分配是我考虑过的,但程序需要动态实例化类(使用虚拟函数),所以我无法预先实例化这些类。也不能重复使用对象。我想现在唯一的选择就是使用可扩展的内存分配器了。谢谢 :) - Nav

3

创建自己的非多线程新内存分配器,每个线程都有其独特的副本。

(您可以覆盖 new 和 delete )

因此,它通过大块分配工作,并且不需要任何锁定,因为每个线程都拥有它自己的内存块。

将您的线程限制为可用核心数。


好的,也许那是典型问题,但它并没有回答这个问题。 - Olhovsky

2

使用new操作符进行内存分配会存在阻塞问题,因为它需要查找下一个可用的内存位,如果有很多线程同时请求内存,这将变得非常棘手。

内存分配速度较慢 - 如果你需要频繁进行内存分配,特别是在多线程环境下,则需要重新设计。你可以在开始时预先分配足够的空间,或者只分配一个大块内存,然后自己进行划分。


不行。我正在使用虚函数并复制许多包含boost矩阵的对象。因此,必须动态分配内存。我想“重新设计”是唯一的选择。 - Nav
"内存分配速度较慢"这在很大程度上取决于平台。使用标准的Visual Studio CRT,我已经习惯了这种情况,但最近我开始使用可扩展的分配器,令我惊讶的是它们的性能非常出色 - 大多数分配器即使在单线程使用时也可以显著降低内存分配成本,并且在多核心方面具有出色的可伸缩性。请参见下面的答案。 - Suma
@Suma:与堆栈或预分配相比较慢。 - murrekatt
@Suma - 而且与不这样做相比,速度很慢啊;-) - Martin Beckett
我只是想指出,一些现代可扩展的分配器通常接近于“使用'new'分配一个大块,然后自己进行分区?”除非它们遇到了某些对它们来说病态的模式,而使用它们可以为您提供几乎相同的性能,并具有本地和自然语言支持的优雅。 - Suma

1
你需要查看编译器文档,确定它是否支持线程安全的分配器。如果不支持,那么你需要重载 new 操作符并使其线程安全,否则会导致段错误或未定义行为。

这个线程表明,在gcc上new操作符“一般”是线程安全的:https://dev59.com/onRA5IYBdhLWcg3w4R_o - Nav
@Nav:我认为“new”操作符是可重入的,但其线程安全性取决于实现。如果您能发布任何标准文档,我会很高兴看到。 - Arunmu

1
在一些平台上,比如Windows,全局堆的访问是由操作系统串行化的。拥有一个线程分离的堆可以大幅提高分配时间。
当然,在这种情况下,值得质疑的是你是否真正需要堆分配,而不是其他形式的动态分配。

什么是“线程分离堆”?堆分配是动态分配,对吗?还有哪些形式的动态分配可用?http://en.wikipedia.org/wiki/Dynamic_memory_allocation - Nav
1
@Nav:一些操作系统可以创建多个堆。您可以为每个线程分配一个堆。还有不同形式的动态分配,例如对象池。如果您知道对象分配的模式,则可以编写自定义分配器,这样在效率上会更高。现有的堆分配子程序旨在在性能方面具有最大的灵活性。 - Puppy

1

您可能想看一下 The Hoard Memory Allocator:"它是malloc()的替代品,可以显著提高应用程序的性能,特别是在多处理器上运行的多线程程序中。"


0
  1. 您可以尝试并行分配大约8个内存(因为您有8个物理核心),而不是您所写的10000,这是最好的方法。

  2. 标准的malloc使用互斥锁,标准的STL分配器也是如此。因此,当引入线程时,它不会自动加速。 然而,您可以使用另一个malloc库(例如搜索“ptmalloc”)来避免使用全局锁定。如果您使用STL进行分配(例如分配字符串、向量),则必须编写自己的分配器。

相当有趣的文章:http://developers.sun.com/solaris/articles/multiproc/multiproc.html


现在提到互斥锁真的非常有帮助!我想知道它是否是串行发生。八个分配有点令人失望。你不觉得使用其他人提到的堆池可能会更快吗? - Nav
@Nav:嗯,其实没有什么魔法 - 你有8个核心,所以这是你可以达到的并行性。 - Gregory
抱歉,我之前发送了评论。我猜ptmalloc内部使用的是堆池(heap pool)。不认为有任何理由自己实现堆池。附注:我在我的回答中添加了一篇文章的链接。 - Gregory
另一方面,如果您减少实际堆分配的数量,并通过块进行分配,则可以提高性能。这样做总是有帮助的——因为malloc操作相当昂贵。 - Gregory

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