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

问题描述 投票:161回答:16

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

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

c++ memory-management std memory-alignment allocator
16个回答
107
投票

正如我提到here,我看到英特尔TBB的自定义STL分配器只需更改单个版本就可以显着提高多线程应用程序的性能

std::vector<T>

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

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


3
投票

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

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

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


3
投票

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

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

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

还有很多次我可以看到在嵌入式系统的上下文中编写自己的自定义分配器,例如,如果序列的内存不在主ram中,就像在these platforms上经常出现的情况那样。


2
投票

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

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


2
投票

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

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

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


2
投票

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


1
投票

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

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


1
投票

在图形模拟中,我见过自定义分配器

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

76
投票

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

EASTL -- Electronic Arts Standard Template Library


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。随意将它用于您的项目。


24
投票

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


18
投票

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

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


6
投票

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

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


6
投票

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

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

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

5
投票

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

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

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

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

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


4
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.