多线程可以加速内存分配吗?

Posted

技术标签:

【中文标题】多线程可以加速内存分配吗?【英文标题】:Can multithreading speed up memory allocation? 【发布时间】:2011-06-19 01:24:09 【问题描述】:

我正在使用 8 核处理器,并且正在使用 Boost 线程来运行大型程序。 从逻辑上讲,程序可以分成组,每个组由一个线程运行。 在每个组中,一些类调用“new”操作符总共 10000 次。 Rational Quantify 表明,“新”内存分配在程序运行时占用了最大处理时间,并减慢了整个程序的速度。

我可以加快系统速度的一种方法是在每个“组”中使用线程,这样 10000 个内存分配可以并行发生。

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

【问题讨论】:

感谢您分析您的应用程序。 @Everyone:好的,所以“堆争用”是在这方面寻找的正确短语。显然 glibc v2 以后并行处理 malloc citi.umich.edu/projects/linux-scalability/reports/malloc.html 但与 free() 的争用将(可能)仅从版本 2.2.4 开始处理 bozemanpass.com/info/linux/malloc/Linux_Heap_Contention.html。我想知道这是否意味着像 Hoard 这样的库会变得多余。 【参考方案1】:

标准 CRT

虽然在旧版 Visual Studio 中默认 CRT 分配器是阻塞的,但至少对于直接调用相应操作系统函数的 Visual Studio 2010 和更新版本而言,情况不再如此。 Windows 堆管理器在 Widows XP 之前一直处于阻塞状态,在 XP 中是可选的Low Fragmentation Heap is not blocking,而默认的是,较新的操作系统(Vista/Win7)默认使用 LFH。最近(Windows 7)分配器的性能非常好,可与下面列出的可扩展替代品相媲美(如果针对较旧的平台或当您需要它们提供的一些其他功能时,您仍然可能更喜欢它们)。存在多个“可扩展分配器”,具有不同的许可证和不同的缺点。我认为在 Linux 上,默认运行时库已经使用了可扩展的分配器(PTMalloc 的一些变体)。

可扩展替换

我知道:

HOARD(GNU + 商业许可证) MicroQuill SmartHeap for SMP(商业许可) Google Perf Tools TCMalloc(BSD 许可证) NedMalloc(BSD 许可证) JemAlloc(BSD 许可证) PTMalloc(GNU,还没有 Windows 端口?) Intel Thread Building Blocks(GNU,商业)

您可能想查看Scalable memory allocator experiences,了解我在 Windows 项目中尝试使用其中一些的经验。

实际上,它们中的大多数通过每个线程缓存和每个线程为分配预分配的区域来工作,这意味着小分配通常只发生在线程上下文中,操作系统服务很少被调用。

【讨论】:

嘿,谢谢!只是为了添加到列表中,英特尔线程构建模块还具有可扩展性_malloc、可扩展性_免费、可扩展性_realloc、可扩展性_calloc、可扩展性_分配器和缓存_对齐_分配器。 苏玛,这也不对。默认情况下,所有现代 MSVC 版本都使用 OS 堆函数(除非被告知不要这样做)。如果启用低碎片堆,OS 堆函数将执行得相当好,这是自 Windows Vista 以来的默认设置(在 Windows XP 上,应用程序只需调用 HeapSetInformation() 即可启用它)。启用 LFH 后,Windows 堆的性能可以与其他可用的最快分配器相媲美 - 我个人针对 NedMalloc 做了一个基准测试,差异可以忽略不计。 @PaulGroke 你是对的,我已经尝试更新答案。【参考方案2】:

内存的动态分配使用应用程序/模块/进程的堆(但不是线程)。堆一次只能处理一个分配请求。如果您尝试在“并行”线程中分配内存,它们将由堆按适当的顺序处理。你不会得到这样的行为:一个线程正在等待获取它的内存,而另一个可以请求一些,而第三个正在获取一些。线程必须排队才能获得它们的内存块。

您需要的是一个堆池。使用当前不忙的任何堆来分配内存。但是,您必须在此变量的整个生命周期中小心,以免它在另一个堆上被取消分配(这会导致崩溃)。

我知道 Win32 API 具有 GetProcessHeap()、CreateHeap()、HeapAlloc() 和 HeapFree() 等函数,它们允许您创建新堆并从特定的堆 HANDLE 分配/释放内存。我不知道其他操作系统中的等价物(我已经寻找它们,但无济于事)。

当然,您应该尽量避免频繁进行动态分配。但是如果你不能,你可以考虑(为了可移植性)创建你自己的“堆”类(不一定是堆本身,只是一个非常有效的分配器)可以管理大量内存并且肯定一个智能指针类,它将持有对它来自的堆的引用。这将使您能够使用多个堆(确保它们是线程安全的)。

【讨论】:

问题:堆池是指:en.wikipedia.org/wiki/Memory_pool? (我想知道你说的是不是内存池,那么我可以使用 TBB 可扩展分配器。但是自定义分配器已经受到像 Scott Meyers en.wikipedia.org/wiki/Allocator_%28C%2B%2B%29#Custom_allocators 这样的人的抨击) 通过堆池,我的意思是有一个你使用的堆列表(操作系统原生堆,或者自制的,或者来自像 boost 之类的库),你从哪个分配在某个特定时间不忙(即基于忙、可用内存和碎片的优先级队列)。当然,不推荐使用自定义分配器,除非你做的非常仔细和非常好。总而言之,我建议您使用这里其他人建议的一些现成的东西(HOARD 或 TBB 乍一看似乎相当可靠)。 Mikael,你的说法不正确。现代堆实现使用线程缓存等技术来加速并行分配。这意味着与仅使用一个线程相比,您可以使用多个并发线程进行更多的分配。【参考方案3】:

据我所知,有 2 个可扩展的 malloc 替代品:

谷歌的tcmalloc Facebook 的 jemalloc(链接到 performance study 与 tcmalloc 相比)

我对@9​​87654324@(在研究中表现不佳)没有任何经验,但 Emery Berger 潜伏在这个网站上并对结果感到惊讶。他说他会看看,我猜可能是测试或实施的一些细节“困住”了 Hoard,因为一般反馈通常是好的。

请注意jemalloc,当您快速创建然后丢弃线程时,它可能会浪费一点空间(因为它会为您分配的每个线程创建一个新池)。如果你的线程是稳定的,这应该没有任何问题。

【讨论】:

【参考方案4】:

我相信对您问题的简短回答是:是的,可能。正如这里的几个人已经指出的那样,有一些方法可以实现这一目标。

除了您的问题和已在此处发布的答案之外,最好从您对改进的期望开始,因为这几乎可以说明要走哪条路。也许你需要快 100 倍。另外,您是否认为自己在不久的将来也会提高速度,或者是否有足够好的水平?不了解您的应用程序或问题领域,也很难专门为您提供建议。例如,您是否处于需要不断提高速度的问题领域?

在进行性能改进时,首先的一件好事是询问您是否需要按照目前的方式做事?在这种情况下,您能做到吗?预分配对象?系统中是否有最大数量的 X 对象?你能重用对象吗?所有这些都更好,因为您不一定需要在关键路径上进行分配。例如。如果您可以重用对象,则具有预分配对象的自定义分配器会很好地工作。另外,您使用的是什么操作系统?

如果您没有具体的期望或特定水平的表现,只需开始尝试此处的任何建议,您就会发现更多。

祝你好运!

【讨论】:

我考虑过预分配,但是程序需要类的动态实例化(使用虚拟),所以我不能预实例化这些类。也不能重用对象。我想使用可扩展的内存分配器是现在唯一的选择。谢谢:)【参考方案5】:

为您自己的非多线程新内存分配器滚动一个每个线程都有的不同副本。

(您可以覆盖新建和删除)

因此,它以大块的形式进行分配,并且不需要任何锁定,因为每个块都由单个线程拥有。

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

【讨论】:

好吧,也许这是典型的问题,但它不能回答问题。【参考方案6】:

new 几乎是阻塞的,它必须找到下一个空闲的内存位,如果你有很多线程都同时要求这样做,这会很棘手。

内存分配很慢 - 如果您执行多次,尤其是在大量线程上,那么您需要重新设计。能不能一开始就预先分配足够的空间,能不能用'new'分配一大块然后自己分区?

【讨论】:

不。我正在使用虚拟函数并复制许多内部具有增强矩阵的对象。所以内存分配必须动态完成。我想“重新设计”是唯一的选择。 “内存分配很慢”这高度依赖于平台。使用标准的 Visual Studio CRT 我已经习惯了,但最近我开始使用可伸缩分配器,令我惊讶的是它们的性能非常好——即使是单线程使用,它们中的大多数也能显着降低内存分配成本,并且具有出色的多线程可伸缩性核心。请参阅下面的答案。 @Suma:与堆栈或预分配相比慢。 @Suma - 与不这样做相比速度很慢;-) 我只是想指出,一些现代可扩展分配器通常接近于“用'new'分配一个大块,然后自己将其分区?”除非他们遇到了一些对他们来说是病态的模式,并且使用它们保存可以为您提供几乎相同的性能,以及原生和自然语言支持的优雅。【参考方案7】:

您需要检查您的编译器文档是否使分配器线程安全。如果没有,那么您将需要重载 new 运算符并使其成为线程安全的。 否则会导致段错误或 UB。

【讨论】:

嗯,这个线程说 new 在 gcc 上“通常”是线程安全的:***.com/questions/796099/… @Nav :我认为“新”运算符是可重入的,但它的线程安全性取决于实现。如果您可以发布任何标准文档,我将很高兴看到相同的任何标准文档。【参考方案8】:

在某些平台(如 Windows)上,对全局堆的访问由操作系统序列化。拥有一个线程分离的堆可以大大缩短分配时间。

当然,在这种情况下,您是否真的需要堆分配而不是其他形式的动态分配可能值得质疑。

【讨论】:

什么是“线程分离堆”?堆分配是动态分配,对吧?还有哪些其他形式的动态分配可用? en.wikipedia.org/wiki/Dynamic_memory_allocation @Nav:某些操作系统可以创建多个堆。您可以为每个线程分配一个。并且有不同形式的动态分配——例如,对象池。如果您有一个已知的对象分配模式,那么您可能会编写一个更有效的自定义分配器。现有的堆分配子例程旨在使其性能具有最大的灵活性。【参考方案9】:

您可能想看看 The Hoard Memory Allocator:“它是 malloc() 的直接替代品,可以显着提高应用程序性能,尤其是对于在多处理器上运行的多线程程序。”

【讨论】:

【参考方案10】:

    您可以尝试达到的最佳并行内存分配约为 8 个(因为您有 8 个物理内核),而不是您所写的 10000 个

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

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

【讨论】:

现在提到互斥锁非常非常非常有帮助!我想知道它是否连续发生。八次分配有点令人失望。您不认为其他人提到的堆池可以更快地发生吗? @Nav:嗯:没有魔法 - 你有 8 个内核,所以这是你可以达到的并行度。 抱歉,提早发了评论。我想,堆池是 ptmalloc 内部所做的。不要认为你有任何理由自己实现堆池。 PS:在我的答案的文章中添加了一个 lint 另一方面,如果您减少实际堆分配的数量,则按块进行分配会有所帮助。这无论如何都会有所帮助 - 因为 malloc 是相当昂贵的操作。

以上是关于多线程可以加速内存分配吗?的主要内容,如果未能解决你的问题,请参考以下文章

动态内存分配的线程争用

我可以通过使用多个线程来更快地分配内存吗?

C/C++ 的多线程内存分配器

多线程强调内存碎片吗?

eCos 动态内存分配简介

Java内存以及GC