C++中最快的可变堆实现

7
我目前正在寻找能够满足我的需求的最快的C++数据结构:
1. 我有几百万个需要插入的条目。 2. 在每次迭代中,我想要查看最大元素并更新其他10个元素。我甚至只需要降低键值,但我更喜欢可以更新(提高和降低功能)。
我不需要删除/插入(除了初始值)或其他任何东西。我认为堆是首选。在查看STL后,我发现大多数数据结构都不支持更新(这是关键部分)。解决方案是删除和重新插入元素,这似乎非常缓慢(我的程序瓶颈)。然后我看了一下Boost提供的堆,并发现pairing_heap给我带来了最好的结果。所有堆仍然比MultiMap上的删除/插入过程慢。有人有建议吗,我可以尝试哪种其他方法/实现?
感谢您。以下是我当前的计时:
1. MultiMap STL(删除/插入):约70秒 2. Fibonacci Boost:约110秒 3. D-Ary Heap Boost ~(最佳选择:D=150):约120秒 4. Pairing Heap Boost:约90秒 5. Skew Heap Boost:105秒
编辑了我的帖子以澄清一些事情:
1. 我的条目是双倍数(double是键,我仍然附加了一些任意值),所以我认为哈希不是一个好主意。 2. 我说的是PriorityQueue,这是不正确的。相反,第一个实现使用MultiMap,在那里值将被删除,然后重新插入(具有新值)。我更新了我的帖子。对于混淆,对不起。 3. 我不明白std :: make_heap如何解决此问题。 4. 要更新元素,我有一个单独的查找表,其中我维护对元素的句柄。通过这样,我可以直接更新元素而无需搜索它。

有没有可能使用某种哈希算法适合你的需求?如果拥有足够的内存,那么搜索和更新将会更快。而且,你也可以轻松地缓存大小信息。 - Michael Dorgan
1
你能解释一下你是如何更新std::priority_queue中的元素的吗?堆没有搜索功能,而且std::priority_queue甚至没有迭代器。 - Brian Bi
我更新了我的问题来回答一些问题。简要概述一下:我使用双精度浮点数,因此没有真正的必要进行哈希(我认为)。我没有使用 std::priority_queue 而是使用 MultiMap。我研究了 make_heap 但我看不出它如何解决更新问题。有什么想法吗? - user667804
1
使用堆的问题之一是定位对象。您需要某种索引来进入堆(这意味着需要保持某种映射与堆同步),或者必须对数组进行搜索。您可以使用要搜索的对象的值修剪搜索,但最坏情况下搜索仍然是O(n)(我认为期望情况也是如此)。一旦找到对象,使用“向上冒泡”或“向下冒泡”取决于新值,更新很快O(log(n))。但是,这种初始搜索意味着您很难超越传统映射。 - Michael Anderson
你使用哪种键类型?-- 噢,抱歉,我漏掉了。好的,它们有什么值范围?除了 insert 和 erase 之外,你使用了什么 std::multimap 的方法?你需要所有内容都完美排序,还是只需要最大(优先)元素?最后,键的值范围是什么样的?它可以非常不均衡,还是一个相当平衡的分布? - user4842163
显示剩余5条评论
1个回答

4

减少泛化

我尝试了一下这个,因为它很有趣,而且我认为我自己也可以使用这样的结构。通常情况下,如果你不能做出比他们更具限制性(可能还更加健壮)的缩小范围的假设,就极难击败专业编写的标准库。

例如,如果你的目标只是编写一个与mallocfree一样通用、全面的内存分配器,那么要超越它们就非常困难。然而,如果你为特定用例做出很多假设,就很容易编写一个在该特定用例中超越它们的分配器。

这就是为什么,如果你对数据结构有这样的问题,我建议尽可能提供关于特定用例的奇特细节的信息(例如你需要从结构中获得什么,如迭代、排序、搜索、从中间删除、在前面插入、键的范围、类型等)。你想要做什么的一些测试代码(即使是伪代码)也会很有帮助。我们希望尽可能多地做出这些缩小范围的假设。

高度动态内容的次优搜索算法

在你的情况下,我们有一个非常动态的大型堆类型结构。我最初来自于老派游戏界,在这种非常动态的情况下,通常一个次优的搜索算法实际上比一个具有更高算法复杂度的算法要好,因为那种搜索优势往往伴随着更昂贵的构建/更新过程(较慢的插入/删除)。

例如,如果你有大量精灵在2D游戏中每帧移动,甚至一个粗略编写、算法较差的固定网格加速器对于最近邻搜索通常在实践中比算法上更优秀的专业编写的四叉树更好,因为不断移动和重新平衡树的成本会增加超过优秀加速和理论对数复杂度的开销。固定网格在所有精灵聚集在一个区域时会出现病态情况,但这种情况很少发生。

所以我采取了这种策略。我做了一些假设,其中最大的一个是您的键在范围内合理分布。另一个假设是您可以粗略地近似容器应处理的最大元素数量(尽管您可以超过或低于此数字,但对于一些粗略的知识和预期效果最好)。我没有麻烦提供迭代器来遍历容器(如果您想要,但与 std::multimap 不同,键不会被完美排序,它们将“有点”排序),但它确实提供了从中间删除元素以及弹出最小键元素。在我的解决方案中存在一种病理情况,在 std::multimap 中不存在,即如果您有大量元素,其中键的值大致相同(例如:0.000001、0.00000012、0.000000011 等)对于所有百万个元素,它会退化为通过所有元素进行线性搜索,并且执行比 multimap 差得多。

但是,如果我的假设符合您的用例,我得到了一个比 std::multmap 快约 8 倍的解决方案。

注意:这是仓促编写的代码,并带有许多快速而肮脏的分析器支持的微优化,甚至提供了一个池分配器,并以位和字节级别操作事物,使用最大对齐性假设“足够可移植”。它还不会为异常安全等提供麻烦。但是,对于 C++ 对象来说,应该是安全的。

作为测试用例,我创建了一百万个随机键,并开始弹出最小键、更改它们并重新插入它们。我对 multimap 和我的结构执行此操作以比较性能。

平衡分布堆/优先队列(有点类似)

#include <iostream>
#include <cassert>
#include <utility>
#include <stdexcept>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <map>
#include <vector>
#include <malloc.h>

// Max Alignment
#if defined(_MSC_VER)
    #define MAX_ALIGN __declspec(align(16))
#else
    #define MAX_ALIGN __attribute__((aligned(16)))
#endif

using namespace std;

static void* max_malloc(size_t amount)
{
    #ifdef _MSC_VER
        return _aligned_malloc(amount, 16);
    #else
        void* mem = 0;
        posix_memalign(&mem, 16, amount);
        return mem;
    #endif
}

static void max_free(void* mem)
{
    #ifdef _MSC_VER
        return _aligned_free(mem);
    #else
        free(mem);
    #endif
}

// Balanced priority queue for very quick insertions and 
// removals when the keys are balanced across a distributed range.
template <class Key, class Value, class KeyToIndex>
class BalancedQueue
{
public:
    enum {zone_len = 256};

    /// Creates a queue with 'n' buckets.
    explicit BalancedQueue(int n): 
        num_nodes(0), num_buckets(n+1), min_bucket(n+1), buckets(static_cast<Bucket*>(max_malloc((n+1) * sizeof(Bucket)))), free_nodes(0), pools(0)
    {
        const int num_zones = num_buckets / zone_len + 1;
        zone_counts = new int[num_zones];
        for (int j=0; j < num_zones; ++j)
            zone_counts[j] = 0;

        for (int j=0; j < num_buckets; ++j)
        {
            buckets[j].num = 0;
            buckets[j].head = 0;
        }
    }

    /// Destroys the queue.
    ~BalancedQueue()
    {
        clear();
        max_free(buckets);
        while (pools)
        {
            Pool* to_free = pools;
            pools = pools->next;
            max_free(to_free);
        }
        delete[] zone_counts;
    }

    /// Makes the queue empty.
    void clear()
    {
        const int num_zones = num_buckets / zone_len + 1;
        for (int j=0; j < num_zones; ++j)
            zone_counts[j] = 0;
        for (int j=0; j < num_buckets; ++j)
        {
            while (buckets[j].head)
            {
                Node* to_free = buckets[j].head;
                buckets[j].head = buckets[j].head->next;
                node_free(to_free);
            }
            buckets[j].num = 0;
        }
        num_nodes = 0;
        min_bucket = num_buckets+1;
    }

    /// Pushes an element to the queue.
    void push(const Key& key, const Value& value)
    {
        const int index = KeyToIndex()(key);
        assert(index >= 0 && index < num_buckets && "Key is out of range!");

        Node* new_node = node_alloc();
        new (&new_node->key) Key(key);
        new (&new_node->value) Value(value);
        new_node->next = buckets[index].head;
        buckets[index].head = new_node;
        assert(new_node->key == key && new_node->value == value);
        ++num_nodes;
        ++buckets[index].num;
        ++zone_counts[index/zone_len];
        min_bucket = std::min(min_bucket, index);
    }

    /// @return size() == 0.
    bool empty() const
    {
        return num_nodes == 0;
    }

    /// @return The number of elements in the queue.
    int size() const
    {
        return num_nodes;
    }

    /// Pops the element with the minimum key from the queue.
    std::pair<Key, Value> pop()
    {
        assert(!empty() && "Queue is empty!");
        for (int j=min_bucket; j < num_buckets; ++j)
        {
            if (buckets[j].head)
            {
                Node* node = buckets[j].head;
                Node* prev_node = node;
                Node* min_node = node;
                Node* prev_min_node = 0;
                const Key* min_key = &min_node->key;
                const Value* min_val = &min_node->value;
                for (node = node->next; node; prev_node = node, node = node->next)
                {
                    if (node->key < *min_key)
                    {
                        prev_min_node = prev_node;
                        min_node = node;
                        min_key = &min_node->key;
                        min_val = &min_node->value;
                    }
                }
                std::pair<Key, Value> kv(*min_key, *min_val);
                if (min_node == buckets[j].head)
                    buckets[j].head = buckets[j].head->next;
                else
                {
                    assert(prev_min_node);
                    prev_min_node->next = min_node->next;
                }
                removed_node(j);
                node_free(min_node);
                return kv;
            }
        }
        throw std::runtime_error("Trying to pop from an empty queue.");
    }

    /// Erases an element from the middle of the queue.
    /// @return True if the element was found and removed.
    bool erase(const Key& key, const Value& value)
    {
        assert(!empty() && "Queue is empty!");
        const int index = KeyToIndex()(key);
        if (buckets[index].head)
        {
            Node* node = buckets[index].head;
            if (node_key(node) == key && node_val(node) == value)
            {
                buckets[index].head = buckets[index].head->next;
                removed_node(index);
                node_free(node);
                return true;
            }

            Node* prev_node = node;
            for (node = node->next; node; prev_node = node, node = node->next)
            {
                if (node_key(node) == key && node_val(node) == value)
                {
                    prev_node->next = node->next;
                    removed_node(index);
                    node_free(node);
                    return true;
                }
            }
        }
        return false;
    }

private:
    // Didn't bother to make it copyable -- left as an exercise.
    BalancedQueue(const BalancedQueue&);
    BalancedQueue& operator=(const BalancedQueue&);

    struct Node
    {
        Key key;
        Value value;
        Node* next;
    };
    struct Bucket
    {
        int num;
        Node* head;
    };
    struct Pool
    {
        Pool* next;
        MAX_ALIGN char buf[1];
    };
    Node* node_alloc()
    {
        if (free_nodes)
        {
            Node* node = free_nodes;
            free_nodes = free_nodes->next;
            return node;
        }

        const int pool_size = std::max(4096, static_cast<int>(sizeof(Node)));
        Pool* new_pool = static_cast<Pool*>(max_malloc(sizeof(Pool) + pool_size - 1));
        new_pool->next = pools;
        pools = new_pool;

        // Push the new pool's nodes to the free stack.
        for (int j=0; j < pool_size; j += sizeof(Node))
        {
            Node* node = reinterpret_cast<Node*>(new_pool->buf + j);
            node->next = free_nodes;
            free_nodes = node;
        }
        return node_alloc();
    }
    void node_free(Node* node)
    {
        // Destroy the key and value and push the node back to the free stack.
        node->key.~Key();
        node->value.~Value();
        node->next = free_nodes;
        free_nodes = node;
    }
    void removed_node(int bucket_index)
    {
        --num_nodes;
        --zone_counts[bucket_index/zone_len];
        if (--buckets[bucket_index].num == 0 && bucket_index == min_bucket)
        {
            // If the bucket became empty, search for next occupied minimum zone.
            const int num_zones = num_buckets / zone_len + 1;
            for (int j=bucket_index/zone_len; j < num_zones; ++j)
            {
                if (zone_counts[j] > 0)
                {
                    for (min_bucket=j*zone_len; min_bucket < num_buckets && buckets[min_bucket].num == 0; ++min_bucket) {}
                    assert(min_bucket/zone_len == j);
                    return;
                }
            }
            min_bucket = num_buckets+1;
            assert(empty());
        }
    }
    int* zone_counts;
    int num_nodes;
    int num_buckets;
    int min_bucket;
    Bucket* buckets;
    Node* free_nodes;
    Pool* pools;
};

/// Test Parameters
enum {num_keys = 1000000};
enum {buckets = 100000};

static double sys_time()
{
    return static_cast<double>(clock()) / CLOCKS_PER_SEC;
}

struct KeyToIndex
{
    int operator()(double val) const
    {
        return static_cast<int>(val * buckets);
    }
};

int main()
{
    vector<double> keys(num_keys);
    for (int j=0; j < num_keys; ++j)
        keys[j] = static_cast<double>(rand()) / RAND_MAX;

    for (int k=0; k < 5; ++k)
    {
        // Multimap
        {
            const double start_time = sys_time();
            multimap<double, int> q;
            for (int j=0; j < num_keys; ++j)
                q.insert(make_pair(keys[j], j));

            // Pop each key, modify it, and reinsert.
            for (int j=0; j < num_keys; ++j)
            {
                pair<double, int> top = *q.begin();
                q.erase(q.begin());
                top.first = static_cast<double>(rand()) / RAND_MAX;
                q.insert(top);
            }
            cout << (sys_time() - start_time) << " secs for multimap" << endl;
        }

        // Balanced Queue
        {
            const double start_time = sys_time();
            BalancedQueue<double, int, KeyToIndex> q(buckets);
            for (int j=0; j < num_keys; ++j)
                q.push(keys[j], j);

            // Pop each key, modify it, and reinsert.
            for (int j=0; j < num_keys; ++j)
            {
                pair<double, int> top = q.pop();
                top.first = static_cast<double>(rand()) / RAND_MAX;
                q.push(top.first, top.second);
            }
            cout << (sys_time() - start_time) << " secs for BalancedQueue" << endl;
        }
        cout << endl;
    }
}

我的机器上的结果:

3.023 secs for multimap
0.34 secs for BalancedQueue

2.807 secs for multimap
0.351 secs for BalancedQueue

2.771 secs for multimap
0.337 secs for BalancedQueue

2.752 secs for multimap
0.338 secs for BalancedQueue

2.742 secs for multimap
0.334 secs for BalancedQueue

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