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

211

有哪些非常好的理由可以放弃使用 std::allocator 并选择自定义解决方案?您是否遇到过某些情况,其中正确性、性能、可伸缩性等是绝对必要的?有什么非常聪明的例子吗?

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


boost::poolboost::interprocess - Mooing Duck
1
如果你非常熟练,理论上你可以通过分配器在远程机器上使用RAM。 - Mooing Duck
17个回答

138
如我在这里提到的,我曾经看到Intel TBB的自定义STL分配器通过仅仅改变一个单一的东西,显著提高了多线程应用的性能。
std::vector<T>

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

(这是一种快速方便的切换分配器以使用TBB的巧妙线程私有堆的方法;请参阅本文档的第59页链接1

5
谢谢提供第二个链接。使用分配器来实现线程私有堆是很巧妙的。我喜欢这是一个很好的例子,说明在非资源受限(例如嵌入式系统或控制台)的场景下,自定义分配器有明显优势的情况。 - Naaff
8
原始链接已失效,但CiteSeer有PDF版本:http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289 - Arto Bendiken
1
我必须问一下:你能可靠地将这样的向量移动到另一个线程中吗?(我猜想不行) - sellibitze
@sellibitze:由于向量是在TBB任务中进行操作并在多个并行操作中重复使用的,而且无法保证哪个TBB工作线程会选择任务,因此我得出结论它可以正常工作。尽管请注意,TBB在一个线程中创建的东西在另一个线程中释放的历史问题存在(显然是线程私有堆和分配和释放的生产者-消费者模式的经典问题。TBB声称其分配器避免了这些问题,但我看到了相反的情况。也许在更新版本中已经修复了。) - timday
@ArtoBendiken:您的链接中的下载链接似乎无效。 - einpoklum
TBB现在似乎在给定链接的第59页。 - Max C

94

自定义分配器可以在游戏开发中特别有用,尤其是在游戏机上,因为它们只有很少的内存和没有交换空间。在这样的系统中,您希望确保对每个子系统都有严格的控制,以确保一个不重要的子系统不能从关键子系统中窃取内存。其他东西,如池分配器,可以帮助减少内存碎片化。您可以在以下网址找到有关该主题的详细论文:

EASTL -- Electronic Arts Standard Template Library


16
在游戏开发者中,STL 的最基本弱点是 std 分配器设计,正是这个弱点成为创建 EASTL 的最大推动力。对 EASTL 链接点赞(+1)。 - Naaff

82
我正在开发一个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许可证。欢迎在您的项目中使用它。


25
提醒一下,从std::allocator继承并不是编写分配器的惯用方式。你应该查看allocator_traits,它允许您提供最少的功能,而特性类将提供其余部分。请注意,STL总是通过allocator_traits使用您的分配器,而不是直接使用,因此您不需要自己引用allocator_traits。虽然这段代码可能是一个有用的起点,但没有太多动机从std::allocator派生。 - Nir Friedman
@Nir 关于这个主题的好链接:https://learn.microsoft.com/en-us/cpp/standard-library/allocators?view=msvc-170 ... 注意:“警告!在编译时,C++标准库使用allocator_traits类来检测您明确提供了哪些成员,并为任何不存在的成员提供默认实现。不要通过为您的分配器提供allocator_traits的特化来干扰此机制!” - cpurdy

31

我正在使用一种使用C ++编写代码的MySQL存储引擎。我们使用自定义分配器来使用MySQL内存系统而不是与MySQL竞争内存。这使我们可以确保我们正在使用用户配置MySQL要使用的内存,而不是“额外”的内存。


26

使用自定义分配器以使用内存池而非堆可能是有用的。这只是众多示例之一。

对于大多数情况,这确实是一种过早优化。但在某些上下文中(嵌入式设备、游戏等),它可以非常有用。


3
或者,当该内存池被共享时。 - Anthony

15

在使用GPU或其他协处理器时,有时以一种特殊的方式分配主存中的数据结构会更加优势。可以通过自定义分配器方便地实现这种内存的特殊分配方式

使用加速器时通过自定义分配器进行内存分配之所以可行是因为:

  1. 通过自定义分配,加速器运行时或驱动程序将被通知内存块的存在;
  2. 此外,操作系统可以确保已分配的内存块为页面锁定(有些人称其为固定内存),也就是说,操作系统的虚拟内存子系统可能不会在内存中移动或删除页面;
  3. 如果1和2都成立,并且请求在页面锁定内存块和加速器之间进行数据传输,则运行时可以直接访问主存中的数据,因为它知道数据的位置,并且可以确定操作系统没有移动或删除它;
  4. 与非页面锁定内存分配相比,这可以节约一次内存复制:需要将数据从非页面锁定内存复制到页面锁定暂存区,然后加速器才能通过DMA初始化数据传输。

2
不要忘记页面对齐的内存块。如果您正在与驱动程序(例如通过DMA与FPGA通信)交互,并且不想为DMA散列表计算页面偏移量而增加麻烦和开销,那么这将非常有用。 - Jan

11
我没有使用过自定义STL分配器编写C++代码,但我可以想象一个用于响应HTTP请求的C++ Web服务器,它使用自定义分配器来自动删除临时数据。一旦响应生成完成,自定义分配器便可一次性释放所有临时数据。
另一个可能的用途是在编写单元测试时证明函数的行为不依赖于输入的某部分。自定义分配器可以填充内存区域,以任意模式进行测试。

9
似乎第一个例子是析构函数的工作,而不是分配器的工作。 - Michael Dorst
2
如果你担心程序依赖于堆内存的初始内容,那么在valgrind中进行快速(即过夜!)运行将让你知道其中的情况。 - cdyson37
5
@anthropomorphic:析构函数和自定义分配器将一起工作,析构函数将首先运行,然后是自定义分配器的删除操作,这时候不会调用free(...),但是当请求完成后会稍后调用free(...)。这比默认分配器更快,可以减少地址空间的碎片化。 - pts

9

我在这里使用自定义分配器;你甚至可以说它是为了解决其他自定义动态内存管理而存在的。

背景:我们有malloc、calloc、free以及各种operator new和delete的重载版本,链接器很高兴地让STL使用这些。这使我们能够做到自动小对象池、泄漏检测、alloc fill、free fill、带哨兵的填充分配、某些allocs的缓存行对齐以及延迟释放等操作。

问题是,我们在嵌入式环境中运行--没有足够的内存来正确地进行泄漏检测会计工作。至少,在标准RAM中不行--还有另一个通过自定义分配函数可用的RAM堆。

解决方案:编写一个使用扩展堆的自定义分配器,并仅在内存泄漏跟踪架构的内部使用它... 其他所有内容都默认使用执行泄漏跟踪的普通new/delete重载。这避免了跟踪器跟踪自身(并提供了一些额外的打包功能,我们知道跟踪器节点的大小)。

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


7

我正在使用自定义分配器来计算程序中某一部分的分配和释放次数,并测量所需时间。虽然还有其他方式可以实现这一目标,但这种方法对我非常方便。特别有用的是,我可以仅将自定义分配器用于我的容器的子集。


7

自定义分配器是一种安全地擦除内存的合理方式,然后再将其解除分配。

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)


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