【问题标题】:Compelling examples of custom C++ allocators?令人信服的自定义 C++ 分配器示例?
【发布时间】:2010-10-24 00:25:10
【问题描述】:

放弃std::allocator 转而采用自定义解决方案的真正原因有哪些?您是否遇到过对正确性、性能、可扩展性等绝对必要的情况?有什么非常聪明的例子吗?

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

【问题讨论】:

标签: c++ memory-management std memory-alignment allocator


【解决方案1】:

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

std::vector<T>

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

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

【讨论】:

  • 感谢第二个链接。使用分配器来实现线程私有堆是聪明的。我喜欢这是一个很好的例子,说明自定义分配器在不受资源限制(嵌入或控制台)的场景中具有明显优势。
  • 原始链接现已失效,但 CiteSeer 有 PDF:citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
  • 我不得不问:你能可靠地将这样的向量移动到另一个线程中吗? (我猜没有)
  • @sellibitze:由于向量是在 TBB 任务中进行操作并在多个并行操作中重复使用,并且不能保证哪个 TBB 工作线程会接收任务,所以我认为它工作得很好。尽管注意到 TBB 释放在另一个线程中的一个线程上创建的东西存在一些历史问题(显然是线程私有堆和分配和释放的生产者 - 消费者模式的经典问题。TBB 声称它的分配器避免了这些问题,但我看到了其他情况. 也许在较新的版本中已修复。)
  • @ArtoBendiken:您链接中的下载链接似乎无效。
【解决方案2】:

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

EASTL -- Electronic Arts Standard Template Library

【讨论】:

  • +1 for EASTL 链接:“在游戏开发者中,[STL] 最基本的弱点是标准分配器设计,而正是这个弱点是创建 EASTL 的最大因素。 "
【解决方案3】:

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

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

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

【讨论】:

  • 请注意,从 std::allocator 派生并不是编写分配器的真正惯用方式。相反,您应该查看 allocator_traits,它允许您提供最少的功能,而 traits 类将提供其余的。请注意,STL 总是通过 allocator_traits 使用您的分配器,而不是直接使用,因此您不需要自己参考 allocator_traits 从 std::allocator 派生的动机并不大(尽管无论如何这段代码可能是一个有用的起点)。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

    • 或者,当共享内存池时。
    【解决方案6】:

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

    在使用加速器时,通过加速器运行时进行自定义分配可以带来好处的原因如下:

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

    【讨论】:

    • ...不要忘记页面对齐的内存块。如果您正在与驱动程序(即通过 DMA 使用 FPGA)交谈,并且不希望为 DMA 分散列表计算页内偏移量的麻烦和开销,这将特别有用。
    【解决方案7】:

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

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

    【讨论】:

    • 似乎第一个例子是析构函数的工作,而不是分配器的工作。
    • 如果你担心你的程序依赖于堆内存的初始内容,在 valgrind 中快速(即一夜之间!)运行会让你知道一种或另一种方式。
    • @anthropomorphic: 析构函数和自定义分配器将一起工作,析构函数将首先运行,然后删除自定义分配器,它不会调用 free(...),但免费(...) 将在服务请求完成后被调用。这可以比默认分配器更快并减少地址空间碎片。
    【解决方案8】:

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

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

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

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

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

        【解决方案10】:

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

        我遇到这个问题的地方是 Windows 上的插件架构。例如,如果您通过 DLL 边界传递一个 std::string ,那么该字符串的任何重新分配都发生在它起源的堆中,而不是 DLL 中可能不同的堆中,这一点很重要*。

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

        【讨论】:

        • 如果您跨 DLL 边界传递对象,您应该为双方使用多线程(调试)DLL (/MD(d)) 设置。 C++ 在设计时并未考虑到模块支持。或者,您可以屏蔽 COM 接口后面的所有内容并使用 CoTaskMemAlloc。这是使用未绑定到特定编译器、STL 或供应商的插件接口的最佳方式。
        • 老家伙的规则是:不要这样做。不要在 DLL API 中使用 STL 类型。并且不要跨 DLL API 边界传递动态内存释放责任。没有 C++ ABI - 因此,如果您将每个 DLL 视为 C API,您就可以避免一整类潜在问题。当然,以牺牲“C++ 美”为代价。或者正如其他评论所暗示的那样:使用 COM。只使用普通的 C++ 是个坏主意。
        【解决方案11】:

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

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

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

        【讨论】:

        • 他的介绍非常好。我希望有一天他的想法会在 C++ 标准库中实现。我对编写分配器比较陌生,但似乎他在可扩展架构和效率方面有很多非常好的观点,这不仅与游戏引擎程序员有关。
        【解决方案12】:

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

        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)

        【讨论】:

          【解决方案13】:

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

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

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

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

          【讨论】:

            【解决方案14】:

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

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

            【讨论】:

              【解决方案15】:

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

              【讨论】:

                【解决方案16】:

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

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

                【讨论】:

                  【解决方案17】:

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

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

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-10-24
                    • 2023-02-20
                    • 2019-08-11
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多