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

时间:2009-05-05 19:36:49

标签: c++ memory-management std memory-alignment allocator

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

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

17 个答案:

答案 0 :(得分:107)

正如我提到here,我看到英特尔TBB的自定义STL分配器只需更改一个

即可显着提高多线程应用的性能
std::vector<T>

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

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

答案 1 :(得分:76)

自定义分配器有用的一个领域是游戏开发,尤其是游戏控制台,因为它们只有少量内存而且没有交换。在这样的系统上,您需要确保对每个子系统进行严格控制,这样一个不加批判的系统就无法从关键系统中窃取内存。池分配器等其他功能可以帮助减少内存碎片。您可以在以下网址找到关于该主题的详细论文:

EASTL -- Electronic Arts Standard Template Library

答案 2 :(得分:59)

我正在研究一个允许向量使用内存的mmap-allocator 内存映射文件。目标是使用存储的向量 直接在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。随意将它用于您的项目。

答案 3 :(得分:24)

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

答案 4 :(得分:18)

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

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

答案 5 :(得分:6)

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

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

答案 6 :(得分:6)

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

使用加速器时通过加速器运行时进行自定义分配的原因如下:

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

答案 7 :(得分:5)

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

背景:我们有malloc,calloc,free以及operator new和delete的各种变体的重载,并且链接器很高兴让STL为我们使用这些。这让我们可以做一些事情,比如自动小对象池,泄漏检测,分配填充,免费填充,带句子的填充分配,某些分配的缓存行对齐以及延迟释放。

问题是,我们正在嵌入式环境中运行 - 没有足够的内存来实际在长时间内正确地进行泄漏检测。至少,不是在标准RAM中 - 通过自定义分配功能,可以在其他地方使用另一堆RAM。

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

出于同样的原因,我们还使用它来保存功能成本分析数据;为每个函数调用和返回写入一个条目,以及线程切换,可以快速获得成本。自定义分配器再次在较大的调试存储区中为我们提供较小的分配。

答案 8 :(得分:4)

我正在使用自定义分配器来计算程序的一部分中的分配/解除分配的数量,并测量它需要多长时间。还有其他方法可以实现,但这种方法对我来说非常方便。特别有用的是我可以将自定义分配器仅用于我的容器的子集。

答案 9 :(得分:3)

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

我碰到的是Windows上的插件架构。例如,如果你跨越DLL边界传递一个std :: string,那么字符串的任何重新分配都是从它所源自的堆发生的,而不是DLL中可能不同的堆*,这是至关重要的。

* 它实际上比这更复杂,就像你动态链接到CRT一样,这可能会起作用。但是,如果每个DLL都有一个到CRT的静态链接,那么你将面临一个痛苦的世界,在这个世界中,幻象分配错误不断发生。

答案 10 :(得分:3)

我曾经使用过这些的一个例子是使用资源非常有限的嵌入式系统。让我们说你有2k的ram免费,你的程序必须使用一些内存。您需要将4-5个序列存储在不在堆栈中的某个位置,此外,您需要对存储这些内容的位置进行非常精确的访问,这种情况下您可能需要编写自己的分配器。默认实现可能会破坏内存,如果您没有足够的内存且无法重新启动程序,这可能是不可接受的。

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

所以我们所做的就是实现我们自己的memory pool。每个内存块都足够大,以适应我们需要的最大序列。这提前分配了固定大小的内存块,并标记了当前正在使用的内存块。我们通过保留一个8位整数来完成此操作,其中如果使用某个块,则表示每个位。我们在这里交换了内存使用情况,试图让整个过程更快,在我们的情况下这是合理的,因为我们正在推动这个微控制器芯片接近它的最大处理能力。

在其他时候,我可以看到在嵌入式系统的上下文中编写自己的自定义分配器,例如,如果序列的内存不在主ram中,则可能经常出现在these platforms

答案 11 :(得分:2)

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

Boost::Interprocess的分配器就是一个很好的例子。但是,正如你可以阅读here所有这些都不够,使所有STL容器共享内存兼容(由于不同进程中的不同映射偏移,指针可能&#34;中断&#34;)。

答案 12 :(得分:2)

与Andrei Alexandrescu的CppCon 2015关于分配者的谈话的强制性链接:

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

好消息是,只要设计它们就会让你想到如何使用它们的想法: - )

答案 13 :(得分:2)

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

答案 14 :(得分:2)

自定义分配器是在释放内存之前安全擦除内存的合理方法。

template <class T>
class allocator
{
public:
    using value_type    = T;

    allocator() noexcept {}
    template <class U> allocator(allocator<U> const&) noexcept {}

    value_type*  // Use pointer if pointer is not a value_type*
    allocate(std::size_t n)
    {
        return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
    }

    void
    deallocate(value_type* p, std::size_t) noexcept  // Use pointer if pointer is not a value_type*
    {
        OPENSSL_cleanse(p, n);
        ::operator delete(p);
    }
};
template <class T, class U>
bool
operator==(allocator<T> const&, allocator<U> const&) noexcept
{
    return true;
}
template <class T, class U>
bool
operator!=(allocator<T> const& x, allocator<U> const& y) noexcept
{
    return !(x == y);
}

推荐使用 Hinnant 的分配器样板: https://howardhinnant.github.io/allocator_boilerplate.html)

答案 15 :(得分:1)

我个人使用Loki :: Allocator / SmallObject来优化小对象的内存使用 - 如果你必须处理适量的非常小的对象(1到256字节),它会显示出良好的效率和令人满意的性能。如果我们谈论分配适量的不同大小的小对象,它可以比标准C ++新/删除分配高出约30倍。此外,还有一个名为&#34; QuickHeap&#34;的特定于VC的解决方案,它带来了最佳性能(分配和解除分配操作只需读取和写入分配/返回到堆的块的地址,分别在up中(9)%的情况 - 取决于设置和初始化),但是以显着的开销为代价 - 每个范围需要两个指针,每个新的内存块需要一个额外的指针。如果你不需要大量的对象大小(它为每个对象大小创建一个单独的池,那么它是一个最快的解决方案,用于处理大量(10 000 ++)数量的对象被创建和删除在当前实现中从1到1023字节,因此初始化成本可能会降低整体性能提升,但是在应用程序进入其性能关键阶段之前,可以继续分配/解除分配一些虚拟对象。

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

答案 16 :(得分:1)

在图形模拟中,我见过用于

的自定义分配器
  1. std::allocator没有直接支持的对齐约束。
  2. 通过使用短期池(仅此框架)和长期分配来最小化碎片。