自定义c++分配器的引人注目的例子?

有什么真正好的理由来抛弃std::allocator,而支持自定义解决方案?您是否遇到过这样的情况:它对于正确性、性能、可伸缩性等来说是绝对必要的?有什么聪明的例子吗?

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

117321 次浏览

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

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

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

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

正如我所提到的在这里,我已经看到英特尔TBB的自定义STL分配器显著提高了多线程应用程序的性能,只需要更改单个STL分配器

std::vector<T>

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

(这是一种快速方便的方式切换分配器使用TBB的漂亮的线程私有堆;见本文档第7页)

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

自定义分配器可以发挥作用的一个领域是游戏开发,特别是在游戏机上,因为它们只有少量内存,没有交换空间。在这样的系统上,您要确保对每个子系统都有严格的控制,这样一个不重要的系统就不能从一个重要的系统窃取内存。池分配器等其他功能可以帮助减少内存碎片。你可以在这里找到一篇关于这个主题的详细的长篇论文:

EASTL - Electronic Arts标准模板库

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

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

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

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

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

我正在使用一个自定义分配器来计算程序的一部分中的分配/释放的数量,并测量它需要多长时间。还有其他方法可以达到这个目的,但这个方法对我来说非常方便。特别有用的是,我只能对容器的一个子集使用自定义分配器。

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

我在Windows上的插件架构中遇到了这种情况。例如,如果你跨DLL边界传递一个std::string,任何字符串的重新分配都发生在它起源的堆中,而不是在DLL中的堆中,这可能是不同的*。

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

我正在研究一个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>());

它可以用于记录分配内存的时间。什么是必要的 是重新绑定结构,否则向量容器使用超类分配/释放 方法。< / p >

更新:内存映射分配器现在在https://github.com/johannesthoma/mmap_allocator中可用,并且是LGPL。您可以在项目中使用它。

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

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

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

我个人使用Loki::Allocator / SmallObject来优化小对象的内存使用——如果你必须处理适量的小对象(1到256字节),它会显示出良好的效率和令人满意的性能。如果我们讨论分配适量的不同大小的小对象,它可以比标准c++的new/delete分配效率高30倍。此外,有一个vc特有的解决方案叫做“QuickHeap”,它带来了最好的性能(分配和释放操作只是读取和写入被分配/返回到堆的块的地址,分别在高达99.9 %的情况下-取决于设置和初始化),但代价是显著的开销-每个区段需要两个指针,每个新的内存块需要一个额外的指针。如果你不需要各种各样的对象大小(它为每个对象大小创建一个单独的池,在当前的实现中从1到1023字节,所以初始化成本可能会贬低整体性能的提升,但你可以在应用程序进入性能关键阶段之前分配/释放一些虚拟对象),这是一种最快的解决方案。

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

对于共享内存来说,不仅容器头存储在共享内存中,而且容器头包含的数据也存储在共享内存中,这一点至关重要。

Boost::进程间的分配器就是一个很好的例子。然而,正如你可以读取在这里,这个allone是不够的,要使所有STL容器共享内存兼容(由于不同进程中的映射偏移量不同,指针可能会“中断”)。

我曾经使用过的一个例子是在资源非常有限的嵌入式系统中工作。假设你有2k的内存,而你的程序必须使用其中的一些内存。你需要在堆栈之外的地方存储4-5个序列另外,你需要非常精确地访问这些东西的存储位置,这种情况下你可能想要编写自己的分配器。默认的实现会造成内存碎片,如果你没有足够的内存并且不能重新启动你的程序,这可能是不可接受的。

我参与的一个项目是在一些低功耗芯片上使用AVR-GCC。我们必须存储8个可变长度但已知最大值的序列。标准库实现内存管理是malloc/free的一个精简包装器,它通过在每个已分配的内存块前加上一个指向已分配内存块末端的指针来跟踪放置项的位置。当分配一个新的内存块时,标准分配器必须遍历每一个内存块,以找到下一个可用的块,以满足所请求的内存大小。在桌面平台上,这对于这几个项目来说是非常快的,但你必须记住,相比之下,这些微控制器中的一些非常缓慢和原始。此外,内存碎片问题是一个巨大的问题,这意味着我们别无选择,只能采取不同的方法。

所以我们所做的就是实现我们自己的内存池。每个内存块都足够大,可以容纳我们需要的最大序列。这将提前分配固定大小的内存块,并标记当前正在使用的内存块。我们通过保留一个8位整数来做到这一点,如果使用某个块,则每个位表示。为了让整个过程更快,我们在这里权衡了内存使用,在我们的情况下,这是合理的,因为我们正在推动这个微控制器芯片接近它的最大处理能力。

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

强制性链接到Andrei Alexandrescu 2015年CppCon关于分配者的演讲:

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

好处是,只是设计它们让你想到如何使用它们:-)

在图形模拟中,我看到自定义分配器用于

  1. std::allocator不直接支持的对齐约束。
  2. 通过为短期分配(只是这个框架)和长期分配使用单独的池来最小化碎片。

以前我发现这个解决方案对我非常有用:STL容器的快速c++ 11分配器。它略微加快了VS2017上的STL容器(~5倍)以及GCC上的STL容器(~7倍)。它是一种基于内存池的特殊用途的分配器。它可以与STL容器一起使用,这多亏了您所要求的机制。

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

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) < / p >