自定义C ++分配器的引人注目的示例?


176

std::allocator放弃使用定制解决方案的真正理由是什么?您是否遇到过绝对需要正确性,性能,可伸缩性等的情况?有没有真正聪明的例子?

自定义分配器一直是我不需要的标准库的功能。我只是想知道SO上的任何人是否可以提供一些令人信服的示例来证明它们的存在。

Answers:


121

正如我在这里提到的,我已经看到英特尔TBB的自定义STL分配器只需更改一个即可显着提高多线程应用程序的性能。

std::vector<T>

std::vector<T,tbb::scalable_allocator<T> >

(这是切换分配器以使用TBB的漂亮的线程专用堆的快速便捷的方法;请参阅本文档的第7页


3
感谢您的第二个链接。使用分配器实现线程专用堆很聪明。我喜欢这是一个很好的示例,说明自定义分配器在不受资源限制(嵌入式或控制台)的情况下具有明显的优势。
纳夫

7
原始链接现在已失效,但CiteSeer具有PDF:citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
Arto Bendiken

1
我不得不问:您能可靠地将这样的向量移到另一个线程中吗?(我猜不是)
sellibitze 2014年

@sellibitze:由于向量是在TBB任务中进行操纵并在多个并行操作中重用的,并且不能保证哪个TBB工作线程将执行任务,因此我得出结论,它工作得很好。尽管注意到TBB释放一个线程在另一个线程中创建的东西是一些历史性的问题(显然是线程私有堆和生产者-消费者模式的分配与释放模式的经典问题。TBB声称它的分配器避免了这些问题,但是我看到了其他情况(也许已在较新版本中修复。)
2014年

@ArtoBendiken:您链接上的下载链接似乎无效。
einpoklum

81

自定义分配器可能有用的一个领域是游戏开发,尤其是在游戏机上,因为它们只有少量的内存并且没有交换。在这样的系统上,您需要确保对每个子系统都具有严格的控制权,以使一个非关键系统无法从关键系统中窃取内存。诸如池分配器之类的其他东西可以帮助减少内存碎片。您可以在以下位置找到有关该主题的详细文章:

EASTL-电子艺术标准模板库


14
EASTL链接+1:“游戏开发人员中[STL]最根本的弱点是std分配器设计,而正是这个弱点是EASTL创建的最大原因。”
纳夫2009年

65

我正在使用mmap分配器,该分配器允许向量使用内存映射文件中的内存。目标是使向量使用的存储直接位于mmap映射的虚拟内存中。我们的问题是要在不增加复制开销的情况下,将非常大的文件(> 10GB)读入内存,因此我需要此自定义分配器。

到目前为止,我已经有了一个自定义分配器的框架(该分配器派生自std :: allocator),我认为这是编写自己的分配器的一个很好的起点。随意使用您想要的任何方式使用这段代码:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

要使用它,请声明一个STL容器,如下所示:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

例如,它可用于在分配内存时记录日志。必需的是重新绑定结构,否则向量容器将使用超类的分配/取消分配方法。

更新:内存映射分配器现在可以在https://github.com/johannesthoma/mmap_allocator上找到,并且是LGPL。随时将其用于您的项目。


17
从std :: allocator派生出来的只是一个提示,实际上并不是编写分配器的惯用方式。相反,您应该查看allocator_traits,它允许您提供最少的功能,而traits类将提供其余功能。请注意,STL始终通过allocator_traits而不是直接使用allocator,因此您无需自己引用allocator_traits。从std :: allocator派生的动机并不多(尽管此代码可能是一个有用的起点)。
尼尔·弗里德曼

25

我正在使用使用c ++作为其代码的MySQL存储引擎。我们正在使用自定义分配器来使用MySQL内存系统,而不是与MySQL竞争内存。它使我们能够确保使用的内存是用户配置的要使用的MySQL,而不是“额外”。


20

使用自定义分配器来使用内存池而不是堆可能会很有用。这是许多其他例子中的一个。

对于大多数情况,这肯定是过早的优化。但是它在某些情况下(嵌入式设备,游戏等)可能非常有用。


3
或者,当共享该内存池时。
安东尼

9

我没有用自定义STL分配器编写C ++代码,但是我可以想象一个用C ++编写的Web服务器,它使用自定义分配器自动删除响应HTTP请求所需的临时数据。一旦生成响应,定制分配器就可以一次释放所有临时数据。

自定义分配器(我已经使用过)的另一个可能用例是编写一个单元测试,以证明函数的行为不依赖于其输入的某些部分。自定义分配器可以用任何模式填充内存区域。


5
似乎第一个示例是析构函数的工作,而不是分配程序的工作。
2014年

2
如果您担心程序依赖于堆中内存的初始内容,则在valgrind中快速运行(即通宵!)将使您知道一种或另一种方式。
cdyson37'3

3
@anthropomorphic:析构函数和自定义分配器将一起工作,析构函数将首先运行,然后删除自定义分配器,这将不会调用free(...),但是会调用free(...)稍后,当请求完成时。这可以比默认分配器更快,并减少地址空间碎片。
pt

8

与GPU或其他协处理器一起使用时,以特殊方式在主内存中分配数据结构有时是有益的。这种分配内存的特殊方式可以以方便的方式在自定义分配器中实现。

使用加速器时,通过加速器运行时进行自定义分配可能会有所帮助的原因如下:

  1. 通过自定义分配,将内存块通知加速器运行时或驱动程序
  2. 另外,操作系统可以确保分配的内存块是页面锁定的(有人称此为固定内存),也就是说,操作系统的虚拟内存子系统可能不会在内存中移动页面或从内存中删除页面。
  3. 如果1.和2.保持,并且请求在页面锁定的内存块和加速器之间进行数据传输,则运行时可以直接访问主内存中的数据,因为它知道它在哪里,并且可以确保操作系统没有移动/删除
  4. 这样可以保存一个以非页面锁定方式分配的内存所发生的内存副本:必须将数据从主存储器中复制到页面锁定的暂存区,然后加速器才能初始化数据传输(通过DMA) )

1
...不要忘记页面对齐的内存块。如果您正在与驱动程序对话(即通过DMA与FPGA对话)并且不想为DMA散列表计算页内偏移量的麻烦和开销,则此功能特别有用。
1

7

我在这里使用自定义分配器;你甚至可能说这是要解决其他自定义动态内存管理问题。

背景:malloc,calloc,free和运算符new和delete的各种变体都有重载,而链接程序很高兴使STL为我们使用它们。这使我们能够执行以下操作:自动小对象池化,泄漏检测,分配填充,空闲填充,带有哨兵的填充分配,某些分配的缓存行对齐以及延迟释放。

问题是,我们正在嵌入式环境中运行-内存不足,无法在很长一段时间内正确地进行泄漏检测统计。至少不是在标准RAM中,而是通过自定义分配功能在其他地方提供了另一堆RAM。

解决方案:编写一个使用扩展堆的自定义分配器,并且在内存泄漏跟踪体系结构的内部使用它。这避免了跟踪器自身跟踪(并且还提供了一些额外的打包功能,我们知道跟踪器节点的大小)。

出于相同的原因,我们还使用它来保存功能成本分析数据。为每个函数调用和返回以及线程切换编写一个条目会很快变得昂贵。自定义分配器再次在较大的调试内存区域中为我们提供了较小的分配。


5

我正在使用自定义分配器来计算程序的一部分中分配/取消分配的数量并测量所需的时间。还有其他方法可以实现,但是这种方法对我来说非常方便。我只能对容器的一部分使用自定义分配器,这特别有用。


4

一种重要情况:编写必须跨模块(EXE / DLL)边界工作的代码时,必须仅在一个模块中进行分配和删除操作,这一点很重要。

我遇到的地方是Windows上的插件体系结构。至关重要的是,例如,如果跨DLL边界传递std :: string,则该字符串的任何重新分配都应从其起源的堆而不是DLL中可能不同的堆中进行*。

* 实际上,这比这要复杂得多,因为好像您正在动态链接到CRT一样,它仍然可以工作。但是,如果每个DLL都具有到CRT的静态链接,那么您将陷入痛苦的世界,幻像分配错误不断发生。


如果跨DLL边界传递对象,则应在两侧都使用多线程(调试)DLL(/ MD(d))设置。C ++在设计时并未考虑模块支持。或者,您可以屏蔽COM接口背后的所有内容并使用CoTaskMemAlloc。这是使用未绑定到特定编译器,STL或供应商的插件接口的最佳方法。
gast128

老人的规则是:不要这样做。不要在DLL API中使用STL类型。并且不要跨DLL API边界传递动态无内存责任。没有C ++ ABI-因此,如果将每个DLL都视为C API,则可以避免一整套潜在的问题。当然,以“ c ++的美”为代价。或如其他评论所建议:使用COM。单纯的C ++是一个坏主意。
BitTickler

3

我曾经使用这些工具的一个例子是使用资源非常有限的嵌入式系统。假设您有2k的可用内存,而您的程序必须使用某些内存。您需要将4-5个序列存储在不在堆栈中的某个位置,此外,您还需要非常精确地访问这些内容的存储位置,在这种情况下,您可能需要编写自己的分配器。默认的实现可能会使内存碎片化,如果您没有足够的内存并且无法重新启动程序,那么这可能是不可接受的。

我正在研究的一个项目是在一些低功耗芯片上使用AVR-GCC。我们必须存储8个可变长度的序列,但是具有已知的最大值。内存管理标准库实现是围绕malloc / free的瘦包装器,它通过在每个已分配的内存块前添加一个指向刚超过该已分配内存末尾的指针来跟踪放置项目的位置。在分配新的内存块时,标准分配器必须遍历每个内存块,以找到下一个可用块,该块将适合请求的内存大小。在台式机平台上,这对于这几个项目而言非常快,但是您必须记住,这些微控制器中有些相对较慢且原始。另外,内存碎片问题是一个巨大的问题,这意味着我们真的别无选择,只能采取其他方法。

所以我们要做的是实现我们自己的内存池。每个内存块都足够大,可以容纳我们所需的最大序列。这样会提前分配固定大小的内存块,并标记当前正在使用的内存块。我们通过保留一个8位整数来做到这一点,其中每个位代表是否使用了某个块。我们在此处权衡了内存使用情况,以试图使整个过程更快,在我们的案例中,这是有道理的,因为我们正在将该微控制器芯片推向接近其最大处理能力的水平。

在嵌入式系统的上下文中,还有很多次我可以看到编写自己的自定义分配器,例如,如果序列的内存不在主内存中,例如在这些平台上经常会出现这种情况。



2

对于共享内存,至关重要的是,不仅要将容器头,而且还将其中包含的数据存储在共享内存中。

Boost :: Interprocess的分配器就是一个很好的例子。但是,正如您在这里可以读到的那样,所有这些都不足以使所有STL容器共享内存兼容(由于不同进程中的映射偏移不同,指针可能会“中断”)。



1

我个人使用Loki :: Allocator / SmallObject来优化小对象的内存使用-如果您必须使用少量的小对象(1到256字节),它将显示出良好的效率和令人满意的性能。如果我们谈论分配适量的许多不同大小的小对象,它的效率可能比标准C ++新/删除分配的效率高约30倍。另外,有一个特定于VC的解决方案称为“ QuickHeap”,它带来了最佳的性能(分配和释放操作仅读取和写入正被分配/返回到堆的块的地址,最多可达99.(9)%情况)。 —取决于设置和初始化),但代价是明显的开销—每个扩展区需要两个指针,每个新存储块需要一个指针。它'

标准C ++新增/删除实现的问题在于,它通常只是C malloc / free分配的包装,并且对较大的内存块(例如1024+字节)有效。它在性能方面有时会产生明显的开销,有时还会有用于映射的额外内存。因此,在大多数情况下,自定义分配器的实现方式是最大化性能和/或最小化分配小的(≤1024字节)对象所需的额外内存量。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.