没有内存重新分配的std::set的替代方案是什么?

16

在一个应用程序中,我通过穷举生成了大量的子问题,并使用"std::set"操作来解决它们。为此,我需要"插入"和"查找"元素,并且还需要"迭代"排序列表。

问题在于,对于数百万个子问题中的每一个,"std::set"实现每次插入元素时都会分配新的内存,这使得整个应用程序非常缓慢:

{   // allocate a non-value node
    _Nodeptr _Pnode = this->_Getal().allocate(1); // <- bottleneck of the program

有没有一些STL结构可以让我在执行上述操作时达到“O(log(n))”的时间复杂度,同时不重新分配任何内存?


Unordered_set? - Basilevs
1
Bitset? - Basilevs
7
你有尝试使用自定义分配器,从池中分配内存,而不是使用通用的分配器吗?实际上,我非常想知道这是否有效,但由于问题陈述的限制,我无法验证使用分配器是否会产生任何差异。 - Dietmar Kühl
1
@KarolyHorvath:那是第一条评论... - Johann Gerell
1
我同意使用flat_set或排序向量。忘记O(n),内存分配器是你的开销,而且内存也不是随机访问的(由于缓存)。认真地用排序的std::vector来衡量它。 - BenPope
显示剩余13条评论
2个回答

19

使用自定义分配器似乎是减少构建和释放std::set<...>所需时间的一种方法。下面是一个完整的简单分配器演示,以及一个对结果时间进行分析的程序。

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <memory>
#include <set>
#include <vector>

// ----------------------------------------------------------------------------

template <typename T, std::size_t pool_size = 1024>
class pool_allocator
{
private:
    std::vector<T*> d_pools;
    T*              d_next;
    T*              d_end;
public:
    template <typename O>
    struct rebind {
        typedef pool_allocator<O, pool_size> other;
    };
    pool_allocator(): d_next(), d_end() {}
    ~pool_allocator() {
        std::for_each(this->d_pools.rbegin(), this->d_pools.rend(),
                      [](T* memory){ operator delete(memory); });
    }
    typedef T value_type;
    T*   allocate(std::size_t n) {
        if (std::size_t(this->d_end - this->d_next) < n) {
            if (pool_size < n) {
                // custom allocation for bigger number of objects
                this->d_pools.push_back(static_cast<T*>(operator new(sizeof(T) * n)));
                return this->d_pools.back();
            }
            this->d_pools.push_back(static_cast<T*>(operator new(sizeof(T) * pool_size)));
            this->d_next = this->d_pools.back();
            this->d_end  = this->d_next + pool_size;
        }
        T* rc(this->d_next);
        this->d_next += n;
        return rc;
    }
    void deallocate(T*, std::size_t) {
        // this could try to recycle buffers
    }
};

// ----------------------------------------------------------------------------

template <typename Allocator>
void time(char const* name, std::vector<int> const& random) {
    std::cout << "running " << name << std::flush;
    using namespace std::chrono;
    high_resolution_clock::time_point start(high_resolution_clock::now());

    std::size_t size(0);
    {
        std::set<int, std::less<int>, Allocator> values;
        for (int value: random) {
            values.insert(value);
        }
        size = values.size();
    }

    high_resolution_clock::time_point end(high_resolution_clock::now());
    std::cout << ": size=" << size << " time="
              << duration_cast<milliseconds>(end - start).count() << "ms\n";
}

// ----------------------------------------------------------------------------

int main()
{
    std::cout << "preparing..." << std::flush;
    std::size_t count(10000000);
    std::vector<int> random;
    random.reserve(count);
    std::generate_n(std::back_inserter(random), count, [](){ return std::rand(); });
    std::cout << "done\n";

    time<std::allocator<int>>("default allocator      ", random);
    time<pool_allocator<int, 32>>("custom allocator (32)  ", random);
    time<pool_allocator<int, 256>>("custom allocator (256) ", random);
    time<pool_allocator<int, 1024>>("custom allocator (1024)", random);
    time<pool_allocator<int, 2048>>("custom allocator (2048)", random);
    time<pool_allocator<int, 4096>>("custom allocator (4096)", random);
    time<std::allocator<int>>("default allocator      ", random);
}

// results from clang/libc++:
// preparing...done
// running default allocator      : size=10000000 time=13927ms
// running custom allocator (32)  : size=10000000 time=9260ms
// running custom allocator (256) : size=10000000 time=9511ms
// running custom allocator (1024): size=10000000 time=9172ms
// running custom allocator (2048): size=10000000 time=9153ms
// running custom allocator (4096): size=10000000 time=9599ms
// running default allocator      : size=10000000 time=13730ms

// results from gcc/libstdc++:
// preparing...done
// running default allocator      : size=10000000 time=15814ms
// running custom allocator (32)  : size=10000000 time=10868ms
// running custom allocator (256) : size=10000000 time=10229ms
// running custom allocator (1024): size=10000000 time=10556ms
// running custom allocator (2048): size=10000000 time=10392ms
// running custom allocator (4096): size=10000000 time=10664ms
// running default allocator      : size=10000000 time=17941ms

1
然而,有一些事情正在发生:当使用池大小为1时,pool_allocator实际上需要做更多的工作,至少在分配方面,但总体上仍然更快!将测量结果分为构建和发布集合两部分,显示在彼此之后立即释放多个缓冲区可以显著提高性能。 - Dietmar Kühl

9

使用自定义分配器与 std::set 结合使用可以很有帮助。如果在构造集合之前知道元素的数量,可以分配一个具有适当大小的原始内存缓冲区,然后覆盖自定义分配器类中的 allocate 方法(使用 std::allocator 作为基类),以便它返回指向缓冲区中地址的指针,而不是调用 new 运算符。这仍然需要进行一次内存分配,但只需要一次。代码可能如下所示:

template<class T, size_t S>
class MyAlloc: public allocator<T>
{
    T *buf;
    size_t ptr;
public:
    MyAlloc()
    {
        buf = (T*)malloc(sizeof(T) * S);
        ptr = 0;
    }

    ~MyAlloc()
    {
        free(buf);
    }

    T* allocate(size_t n, allocator<void>::const_pointer hint=0)
    {
        ptr += n;
        return &buf[ptr - n];
    }

    void deallocate(T* p, size_t n)
    {
        //Do nothing.
    }

    template<class T1>
    struct rebind
    {
        typedef MyAlloc<T1, S> other;
    };
};

虽然自定义分配器的想法是好的,但这个实现并不好,它太简单了。它只能分配一个在编译时固定数量的元素。此外,在allocate()返回无效指针时,没有防止分配超过该数量的保护措施。 - Walter
3
我知道。它更像是一份草图而不是现成可用的实现。 - kraskevich

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