191

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

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

4

17 回答 17

134

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

std::vector<T>

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

(这是切换分配器以使用 TBB 的漂亮线程私有堆的一种快速便捷的方法;请参阅本文档中的第 7 页

于 2009-05-05T19:53:45.840 回答
91

One area where custom allocators can be useful is game development, especially on game consoles, as they have only a small amount of memory and no swap. On such systems you want to make sure that you have tight control over each subsystem, so that one uncritical system can't steal the memory from a critical one. Other things like pool allocators can help to reduce memory fragmentation. You can find a long, detailed paper on the topic at:

EASTL -- Electronic Arts Standard Template Library

于 2009-05-05T20:01:09.240 回答
79

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

例如,它可用于在分配内存时记录日志。必要的是重新绑定结构,否则向量容器使用超类分配/释放方法。

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

于 2012-09-21T11:06:30.657 回答
29

I'm working with a MySQL storage engine that uses c++ for its code. We're using a custom allocator to use the MySQL memory system rather than competing with MySQL for memory. It allows us to make sure we're using memory as the user configured MySQL to use, and not "extra".

于 2009-05-05T20:00:21.770 回答
24

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

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

于 2009-05-05T19:52:33.003 回答
12

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

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

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

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

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

于 2009-05-05T19:52:47.560 回答
8

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

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

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

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

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

于 2009-05-05T20:23:00.217 回答
6

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

我遇到的地方是 Windows 上的插件架构。例如,如果您将 std::string 传递到 DLL 边界,则该字符串的任何重新分配都发生在它起源的堆中,而不是 DLL 中可能不同的堆*。

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

于 2012-03-17T02:47:31.480 回答
6

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

于 2011-05-27T12:09:47.073 回答
5

必须链接到 Andrei Alexandrescu 在 CppCon 2015 上关于分配器的演讲:

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

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

于 2016-01-14T22:54:37.493 回答
5

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

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 )

于 2021-07-20T23:10:43.303 回答
4

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

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

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

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

于 2015-07-11T16:36:08.663 回答
3

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

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

于 2015-05-25T14:12:41.570 回答
3

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

于 2017-11-05T15:35:51.913 回答
2

我个人使用 Loki::Allocator / SmallObject 来优化小对象的内存使用——如果您必须处理中等数量的非常小的对象(1 到 256 字节),它会显示出良好的效率和令人满意的性能。如果我们谈论分配适度数量的许多不同大小的小对象,它可以比标准 C++ 新/删除分配效率高约 30 倍。此外,还有一个名为“QuickHeap”的特定于 VC 的解决方案,它带来了最佳性能(分配和解除分配操作只是读取和写入正在分配/返回到堆的块的地址,在高达 99.(9)% 的情况下) — 取决于设置和初始化),但代价是显着的开销 — 每个范围需要两个指针,每个新内存块需要一个额外的指针。它'

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

于 2015-01-02T18:21:16.177 回答
2

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

  1. std::allocator不直接支持的对齐约束。
  2. 通过对短期(仅此帧)和长期分配使用单独的池来最小化碎片。
于 2016-01-15T00:48:50.440 回答