C++自定义分配器,利用底层内存池

7
我正在使用一个内存池类,它可以重复使用已分配的内存地址,并且有一个自定义的分配器来包装这个类。以下代码片段给出了基本接口的概述。
template<class alloc>
class memory_pool
    : boost::noncopyable,
      public allocator_traits<void>
{
public:
    memory_pool(typename alloc::size_type alloc_size);
    memory_pool(typename alloc::size_type alloc_size, alloc const&);
    template<typename U> memory_pool(typename alloc::size_type alloc_size,
        typename alloc::rebind<U>::other const&);
    virtual ~memory_pool();

    pointer allocate  (); /*throw(std::bad_alloc)*/
    void    collect   ();
    void    deallocate(pointer) throw(); /*noexcept*/
};

pointer allocate()
{/*
    Checks if a suitable chunk of memory is available in a internal linked list.
    If true, then the chunk is returned and the next chunk moves up.
    Otherwise, new memory is allocated by the underlying allocator.
*/}

void deallocate(pointer)
{/*
    Interprets the passed pointer as a chunk of memory and stores it in a linked list.
    Please note that memory isn't actually deallocated.
*/}

void collect()
{/*
    Effectively deallocates the cunks in the linked list.
    This will be called at least once during destruction.
*/}

当然,这种需求是有限的。但在以下情况下非常有用: - 为一个类指定分配器类型,该类以非常简单的方式使用该分配器(例如,避免即使建议分配更大的内存块)。 - 反复分配和释放相同大小的内存。 - 您希望使用分配器的类型非常小(例如,char、short、int等内置类型)。
理论上,实现可以利用内存池,每次需要从底层内存管理器中进行分配时,它会分配实际分配大小的倍数。彼此靠近的对象更适合任何缓存和/或预取算法。 我已经实现了这样一个内存池,并增加了一些开销来处理正确的分配、分割和释放(我们不能释放用户将传递给释放函数的每个地址。我们只需要释放之前已经分配的每个内存块的开始地址)。
我已经使用以下非常简单的代码测试了这两种情况:
std::list<int, allocator<int>> list;

std::clock_t t = std::clock();
for (int i = 0; i < 1 << 16; ++i)
{
    for (int j = 0; j < 1 << 16; ++j)
        list.push_back(j);
    list.unique();
    for (int j = 0; j < 1 << 16; ++j)
        list.pop_back();
}
std::cout << (std::clock() - t) / CLOCKS_PER_SEC << std::endl;
std::list每次调用push_back时都会调用allocactor::allocate(1, 0)unique()确保每个元素都将被访问并与下一个元素进行比较。 然而,结果令人失望。管理块分配内存池所需的最小开销大于系统可能获得的任何优势。
您能想到一种情况,在该情况下它将提高性能吗?
编辑: 当然,它比std::allocator快得多。

请注意,包装分配器无法分配数组。 - 0xbadf00d
2个回答

1

C++0x对于作用域分配器(如内存池)有更好的支持。

对你的代码进行剖析,很难预测这将会带来什么优势,除非你的算法执行非常规则的分配/释放模式,例如LIFO。

当所有分配的对象大小相同时,编写一个非常快速的分配器就非常容易。我曾经写过类似以下的东西:

template <size_t ObjectSize> class allocator {
    // ...
};

template <typename T> class allocator : public allocator <sizeof (T)> {
    // ...
};

在设计分配器之前,您需要确定将分配什么以及如何分配。对于operator new的答案是“任何东西”和“无论如何”,这就是为什么有时它不够优化的原因。如果您不能正确回答这些问题,那么您的分配器可能不会有很大的改进。


引用您的链接中的内容:“可以使用具有状态的分配器,例如持有指向要分配的区域的指针的分配器”。为什么在C++03中不可能呢? - 0xbadf00d
对于STL来说,分配器被定义为每种类型而不是每个实例。你可以编写自己的有状态分配器,但如果在std::vector或其兄弟中使用它们,它们将破坏已建立的库实现。 - spraff
我以前用过这种方法(使用ObjectSize作为编译时表达式)。它很好读,但限制了使用。你可能已经注意到我只在这个阶段使用void* - 0xbadf00d

-1
你能想到一种情况,它会提高性能吗?
如果有大量的分配和释放(每秒10k+),那么不必每次都运行复杂查询以进行分配/释放将受益于延迟分配/释放到组中,但前提是将延迟分配/释放到组中的节省总时间要大于处理组所需的时间(基本上需要用每个单元的节省来摊销组开销)。
对于任何基于节点/指针的结构(如树),使用连续内存都会有所帮助(但仅限于某个程度)。然而,现实世界的好处可能与计划中的大相径庭(或不存在!),这就是为什么在创建此类自定义系统时,您应该对代码进行分析,并已经心中有数(即:没有必要为某些几乎不分配的东西制作自定义池分配器,因为速度提升根本无关紧要)。
然而,像这样的东西对于调试非常有用,因为您可以拥有一个漂亮的接口来标记内存以监视泄漏和覆盖/无效写入,因此即使它与标准系统具有相同的性能,您也可以从其他方面获得收益。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接