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

Compelling examples of custom C++ allocators?

提问人: 提问时间:5/6/2009 最后编辑:5 revs, 2 users 57%Naaff 更新时间:10/23/2023 访问量:126592

问:

有哪些真正好的理由放弃定制解决方案?您是否遇到过任何对于正确性、性能、可伸缩性等绝对必要的情况?有什么非常聪明的例子吗?std::allocator

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

C++ 管理 std 内存对齐 分配器

评论

0赞 Mooing Duck 7/21/2021
boost::p oolboost::interprocess
1赞 Mooing Duck 7/21/2021
如果你非常狡猾,理论上你可以通过分配器在远程机器上使用 RAM。

答:

26赞 Martin Cote #1

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

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

评论

3赞 Anthony 6/19/2012
或者,当该内存池被共享时。
11赞 pts #2

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

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

评论

9赞 Michael Francis 7/31/2014
似乎第一个例子是析构函数的工作,而不是分配器。
2赞 cdyson37 3/2/2015
如果你担心你的程序取决于堆中内存的初始内容,那么在 valgrind 中快速(即一夜之间!)运行会让你知道一种或另一种方式。
5赞 pts 3/3/2015
@anthropomorphic:析构函数和自定义分配器将协同工作,析构函数将首先运行,然后删除自定义分配器,该分配器不会调用 free(...),但稍后会在提供请求时调用 free(...)。这可能比默认分配器更快,并减少地址空间碎片。
138赞 6 revs, 5 users 84%timday #3

正如我在这里提到的,我已经看到英特尔 TBB 的自定义 STL 分配器只需更改单个

std::vector<T>

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

(这是将分配器切换为使用 TBB 漂亮的线程专用堆的一种快速而方便的方法;请参阅本文档中的第 59 页)

评论

5赞 Naaff 5/6/2009
感谢您的第二个链接。使用分配器来实现线程专用堆是很聪明的。我喜欢这是一个很好的例子,说明自定义分配器在不受资源限制的方案(嵌入或控制台)中具有明显的优势。
8赞 Arto Bendiken 4/4/2013
原始链接现已失效,但 CiteSeer 的 PDF:citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
1赞 sellibitze 9/22/2014
我不得不问:你能可靠地将这样的向量移动到另一个线程中吗?(我猜不是)
0赞 timday 9/23/2014
@sellibitze:由于向量是从 TBB 任务中操作的,并在多个并行操作中重用,并且无法保证哪个 TBB 工作线程会拾取任务,因此我得出结论,它工作得很好。虽然请注意,TBB 释放在另一个线程中的一个线程上创建的东西存在一些历史问题(显然是线程私有堆和生产者-消费者分配和释放模式的经典问题。TBB 声称它的分配器避免了这些问题,但我看到的并非如此。也许在较新版本中已修复。
31赞 Thomas Jones-Low #4

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

94赞 2 revs, 2 users 91%Grumbel #5

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

EASTL -- Electronic Arts 标准模板库

评论

16赞 Naaff 5/6/2009
+1 表示 EASTL 链接:“在游戏开发者中,[STL] 最根本的弱点是 std 分配器设计,而正是这个弱点是创建 EASTL 的最大因素。
9赞 leander #6

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

背景:我们有 malloc、calloc、free 以及运算符 new 和 delete 的各种变体的重载,链接器很高兴地让 STL 为我们使用这些。这使我们能够执行诸如自动小对象池、泄漏检测、分配填充、自由填充、使用哨兵进行填充分配、某些分配的缓存行对齐以及延迟自由等操作。

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

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

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

7赞 Jørgen Fogh #7

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

6赞 Stephen #8

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

我遇到这种情况的地方是 Windows 上的插件架构。例如,如果跨 DLL 边界传递 std::string,则字符串的任何重新分配都必须从其来源的堆发生,而不是 DLL 中的堆,后者可能不同*。

*实际上,它比这更复杂,就好像您动态链接到 CRT 一样,无论如何它都可能有效。但是,如果每个DLL都有一个指向CRT的静态链接,那么你就会进入一个痛苦的世界,在那里幻象分配错误不断发生。

评论

0赞 gast128 3/5/2016
如果跨 DLL 边界传递对象,则应对两端使用多线程(调试)DLL (/MD(d)) 设置。C++ 在设计时并没有考虑到模块支持。或者,您可以屏蔽 COM 接口后面的所有内容并使用 CoTaskMemAlloc。这是使用插件接口的最佳方式,这些接口不绑定到特定的编译器、STL 或供应商。
0赞 BitTickler 12/24/2019
老家伙们的规则是:不要这样做。不要在 DLL API 中使用 STL 类型。并且不要跨 DLL API 边界传递动态内存可用责任。没有 C++ ABI,因此,如果将每个 DLL 视为 C API,则可以避免一整类潜在问题。当然,这是以牺牲“c++美感”为代价的。或者正如另一条评论所建议的那样:使用 COM。只是普通的C++是个坏主意。
82赞 4 revs, 3 users 96%Johannes Thoma #9

我正在开发一个 mmap-alocator,它允许向量使用来自 内存映射文件。目标是拥有使用存储的向量 直接位于 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>());

例如,每当分配内存时,它都可以用于记录。什么是必要的 是 rebind 结构,否则向量容器使用超类 allocate/delocate 方法。

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

评论

25赞 Nir Friedman 6/18/2015
请注意,从 std::allocator 派生并不是编写分配器的真正惯用方式。相反,您应该查看 allocator_traits,它允许您提供最低限度的功能,而 traits 类将提供其余部分。请注意,STL 始终通过 allocator_traits 而不是直接使用您的分配器,因此您不需要自己引用 allocator_traits 没有太多的动机从 std::allocator 派生(尽管无论如何,此代码可能是一个有用的起点)。
0赞 cpurdy 3/23/2022
@Nir关于该主题的良好链接:learn.microsoft.com/en-us/cpp/standard-library/......注意:“警告!在编译时,C++标准库使用 allocator_traits 类来检测显式提供了哪些成员,并为不存在的任何成员提供默认实现。不要通过为您的分配者提供专业化的allocator_traits来干扰此机制!
15赞 Sebastian #10

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

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

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

评论

2赞 Jan 11/19/2014
...不要忘记页面对齐的内存块。如果您正在与驱动程序(即通过 DMA 使用 FPGA)通信,并且不希望为 DMA 散点列表计算页内偏移量的麻烦和开销,这将特别有用。
3赞 Fractal Multiversity #11

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

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

3赞 ted #12

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

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

5赞 shuttle87 #13

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

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

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

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

5赞 einpoklum #14

安德烈·亚历山德莱斯库 (Andrei Alexandrescu) 的 CppCon 2015 关于分配器的演讲的强制性链接:

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

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

评论

0赞 alexpanter 11/4/2020
他的演讲非常好。我希望他的想法有一天能在C++标准库中实现。我对编写分配器比较陌生,但似乎他有很多关于可扩展架构和效率的非常好的观点,这不仅与游戏引擎程序员有关。
2赞 Adrian McCarthy #15

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

  1. 不直接支持的对齐约束。std::allocator
  2. 通过对短期(仅此帧)和长期分配使用单独的池来最大程度地减少碎片。
3赞 no one special #16

前段时间我发现这个解决方案对我非常有用:STL 容器的快速 C++11 分配器。它略微加快了 VS2017 (~5x) 和 GCC (~7x) 上的 STL 容器速度。它是一个基于内存池的特殊用途分配器。它只能与 STL 容器一起使用,这要归功于您要求的机制。

7赞 Jarom Nelson #17

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

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)