代码之家  ›  专栏  ›  技术社区  ›  yannis

自定义C++分配器的令人信服的例子?

  •  160
  • yannis  · 技术社区  · 6 年前

    有什么好理由可以放弃 std::allocator 支持自定义解决方案?您是否遇到过对正确性、性能、可伸缩性等绝对必要的情况?有什么很聪明的例子吗?

    自定义分配器一直是标准库的一个特性,我不太需要它。我只是想知道这里是否有人能提供一些令人信服的例子来证明他们的存在。

    16 回复  |  直到 6 年前
        1
  •  104
  •   timday    7 年前

    正如我提到的 here ,我已经看到Intel tbb的自定义stl分配器通过更改单个应用程序显著提高了多线程应用程序的性能。

    std::vector<T>
    

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

    (这是一种将分配器切换为使用tbb的漂亮线程私有堆的快速方便的方法;请参见 page 7 in this document )

        2
  •  76
  •   diciu    15 年前

    自定义分配程序可以使用的一个领域是游戏开发,特别是在游戏控制台上,因为它们只有少量的内存,没有交换。在这样的系统上,您希望确保对每个子系统都有严格的控制,这样一个非关键系统就不能从关键系统中窃取内存。像池分配器之类的其他东西可以帮助减少内存碎片。您可以在以下网站上找到有关此主题的详细长篇论文:

    EASTL -- Electronic Arts Standard Template Library

        3
  •  59
  •   moonlightdock    6 年前

    我正在研究一个允许向量使用内存的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>());
    

    例如,它可以用于在分配内存时进行日志记录。需要什么 是重新绑定结构,否则向量容器使用超类allocate/deallocate 方法。

    更新:内存映射分配器现在位于 https://github.com/johannesthoma/mmap_allocator 并且是LGPL。在你的项目中随意使用它。

        4
  •  24
  •   Thomas Jones-Low    15 年前

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

        5
  •  18
  •   Martin Cote    15 年前

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

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

        6
  •  6
  •   pts    15 年前

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

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

        7
  •  6
  •   Sebastian    10 年前

    当使用GPU或其他协处理器时,有时在主内存中分配数据结构对 特殊方式 . 这个 特殊方式 分配内存的方法可以以一种方便的方式在自定义分配程序中实现。

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

    1. 通过自定义分配,将通知加速器运行时或驱动程序内存块
    2. 此外,操作系统还可以确保分配的内存块被页面锁定(有些人会调用 固定存储器 ,也就是说,操作系统的虚拟内存子系统不能在内存中或从内存中移动或删除页面。
    3. 如果是1。2。保持并请求页锁定内存块和加速器之间的数据传输,运行时可以直接访问主内存中的数据,因为它知道数据在哪里,并且可以确保操作系统没有移动/删除数据。
    4. 这将保存一个内存复制,该复制将与以非页面锁定方式分配的内存一起发生:数据必须从主内存复制到页面锁定的临时区域,加速器可以初始化数据传输(通过DMA)
        8
  •  5
  •   leander    15 年前

    我在这里使用自定义分配器;您甚至可以说它是工作的。 围绕 其他自定义动态内存管理。

    背景:我们有malloc、calloc、free的重载,以及new和delete操作符的各种变体,链接器很乐意让stl为我们使用这些变量。这让我们可以做一些事情,比如自动小对象池、泄漏检测、分配填充、自由填充、用岗哨填充分配、为某些分配缓存线对齐和延迟释放。

    问题是,我们在一个嵌入式环境中运行——周围没有足够的内存来在较长的时间内正确地执行泄漏检测计算。至少,不是在标准RAM中——通过定制的分配函数,在其他地方还有另一堆RAM可用。

    解决方案:编写一个使用扩展堆的自定义分配器,并使用它 只有 在内存泄漏跟踪体系结构的内部…其他所有内容都默认为执行泄漏跟踪的常规新建/删除重载。这就避免了跟踪器跟踪本身(也提供了一些额外的打包功能,我们知道跟踪器节点的大小)。

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

        9
  •  4
  •   Jørgen Fogh    13 年前

    我使用一个定制的分配器来计算程序某个部分中分配/释放的数量,并测量它需要多长时间。还有其他方法可以实现,但这种方法对我来说非常方便。我只能对容器的一个子集使用自定义分配器,这尤其有用。

        10
  •  3
  •   Stephen    12 年前

    一个基本情况是:编写必须跨模块(exe/dll)边界工作的代码时,必须只在一个模块中进行分配和删除。

    我在Windows上遇到了一个插件架构。例如,如果通过dll边界传递std::string,则字符串的任何重新分配都必须发生在其源堆中,而不是dll中可能不同*的堆中。

    * 事实上,这比这要复杂得多,就好像你正在动态链接到CRT,这无论如何都可能有效。但是,如果每个DLL都有一个到CRT的静态链接,那么您将面临一个痛苦的世界,在那里幻影分配错误不断发生。

        11
  •  3
  •   shuttle87    9 年前

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

    我正在研究的一个项目是在一些低功耗芯片上使用AVR-GCC。我们必须存储8个长度可变但最大值已知的序列。这个 standard library implementation of the memory management 是一个围绕malloc/free的薄包装器,它通过在每个分配的内存块前面加上一个指针来跟踪放置项目的位置,指针刚好穿过分配的内存块的末尾。当分配一个新的内存块时,标准的分配程序必须遍历每个内存块,以找到下一个可用的块,该块将适合所请求的内存大小。在桌面平台上,这对于这几个项目来说是非常快的,但是您必须记住,与之相比,这些微控制器中的一些非常缓慢和原始。此外,内存碎片问题是一个巨大的问题,这意味着我们真的别无选择,只能采取不同的方法。

    所以我们所做的就是实现我们自己的 memory pool . 每个内存块都足够大,可以容纳我们需要的最大序列。这会提前分配固定大小的内存块,并标记当前正在使用的内存块。我们通过保持一个8位整数来实现这一点,如果使用了某个块,那么每个位都表示出来。为了使整个过程更快,我们牺牲了这里的内存使用,在我们的例子中,这是合理的,因为我们将这个微控制器芯片推向了它的最大处理能力。

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

        12
  •  2
  •   Ted    9 年前

    对于共享内存,不仅要将容器头以及它所包含的数据存储在共享内存中,这一点至关重要。

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

        13
  •  2
  •   einpoklum    9 年前

    与Andrei Alexandrescu的2015年CPPCON关于分配者的讨论的强制性链接:

    https://www.youtube.com/watch?v=LIb3L4vKZ7U

    好的是,仅仅是设计它们就可以让你想到如何使用它们的想法:—)

        14
  •  2
  •   no one special    7 年前

    不久前,我发现这个解决方案对我非常有用: Fast C++11 allocator for STL containers .它在VS2017(~5x)和GCC(~7x)上略微加快了STL容器的速度。它是一个基于内存池的专用分配器。它只能与STL容器一起使用,这要感谢您所要求的机制。

        15
  •  1
  •   Fractal Multiversity    10 年前

    我个人使用loki::allocator/smallobject来优化小对象的内存使用情况,如果您必须处理适量的非常小的对象(1到256字节),它会显示出良好的效率和令人满意的性能。它可以是高达30倍的效率比标准的C++新/删除分配,如果我们谈论分配适量的小对象的许多不同的大小。此外,还有一个叫做“QuickHeap”的VC特定解决方案,它可以带来最佳的性能(分配和解除分配操作只需读写分配/返回到堆的块的地址,分别在高达99(9)%的情况下,取决于设置和初始化),但以显著的开销为代价,它需要两个P每盘区一个指针,每个新内存块另加一个指针。如果您不需要各种各样的对象大小(它为每个对象大小创建一个单独的池,在当前的实现中从1到1023字节,因此初始化成本可能会降低整体性能的提升,但是可以继续进行),那么它是处理大量(10000++)正在创建和删除的对象的最快的解决方案。并在应用程序进入其性能关键阶段之前分配/取消分配一些虚拟对象)。

    标准C++新的/Delphi实现的问题在于它通常只是C MalOC/FAB分配的包装器,它对于更大的内存块(如1024字节)很有用。在性能方面,它有一个显著的开销,有时还需要额外的内存用于映射。因此,在大多数情况下,自定义分配器的实现方式是最大化性能和/或最小化分配小(_1024字节)对象所需的额外内存量。

        16
  •  1
  •   Adrian McCarthy    9 年前

    在一个图形模拟中,我看到了用于

    1. 路线约束: std::allocator 没有直接支持。
    2. 通过为短期(仅此框架)和长期分配使用单独的池来最小化碎片。