【问题标题】:Why is the STL priority_queue not much faster than multiset in this case?在这种情况下,为什么 STL priority_queue 不比 multiset 快多少?
【发布时间】:2025-11-25 06:30:01
【问题描述】:

我正在比较 STL (g++) priority_queue 的性能,发现 push 和 pop 没有我预期的那么快。见以下代码:

#include <set>
#include <queue>

using namespace std;

typedef multiset<int> IntSet;

void testMap()
{
    srand( 0 );

    IntSet iSet;

    for ( size_t i = 0; i < 1000; ++i )
    {
        iSet.insert(rand());
    }

    for ( size_t i = 0; i < 100000; ++i )
    {
        int v = *(iSet.begin());
        iSet.erase( iSet.begin() );
        v = rand();
        iSet.insert(v);
    }
}

typedef priority_queue<int> IntQueue;

void testPriorityQueue()
{
    srand(0);
    IntQueue q;

    for ( size_t i = 0; i < 1000; ++i )
    {
        q.push(rand());
    }

    for ( size_t i = 0; i < 100000; ++i )
    {
        int v = q.top();
        q.pop();
        v = rand();
        q.push(v);
    }
}

int main(int,char**)
{
   testMap();
   testPriorityQueue();
}

我编译了这个 -O3 然后运行 ​​valgrind --tool=callgrind, KCachegrind testMap 占用总 CPU 的 54% testPriorityQueue 占用 44% 的 CPU

(没有-O3 testMap比testPriorityQueue快很多) testPriorityQueue 似乎花费了大部分时间的函数被调用

void std::__adjust_heap<__gbe_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, long, int, std::less<int> >

该函数似乎是从 pop() 调用中调用的。

这个函数具体做什么?有没有办法通过使用不同的容器或分配器来避免它?

【问题讨论】:

  • 堆缓存不友好吗?至少这是我的总体印象。
  • 而且我认为它们以不可预知的方式分支很多。该函数看起来是负责堆“冒泡”的原因,这是每次删除元素以维持其顺序时必须在堆上执行的 log(n) 操作。
  • CPU% 不是测试性能或速度的有用方法。 __adjust_heap“重新平衡”优先级队列,是处理优先级队列时唯一缓慢的操作。这是优先队列所固有的,我能想到的唯一选择是std::set,它必须以类似的方式平衡。
  • 我今天下午做了一个简单的priority_queue模板,在Linux 64位编译-O3时几乎是std::priority_queue的两倍。

标签: c++ performance stl priority-queue multiset


【解决方案1】:

优先级队列被实现为heap:每次移除头元素时都必须“重新平衡”。在链接的描述中,delete-min 是一个O(log n) 操作,实际上是因为min(或头部)元素是扁平二叉树的

集合通常实现为red-black tree,最小元素将是最左边的节点(所以要么是叶子,要么最多有一个右孩子)。因此,它最多有 1 个要移动的子节点,并且根据允许的不平衡程度,可以在多个 pop 调用中分摊重新平衡。

请注意,如果堆有任何优势,它很可能位于引用位置(因为它是连续的而不是基于节点的)。这正是 可能 callgrind 更难以准确测量的优势,所以我建议在接受这个结果之前运行一些经过的实时基准测试。

【讨论】:

  • min 元素不必是叶子——它可能有一个正确的孩子。
【解决方案2】:

我已经实现了一个优先级队列,它在使用 -O3 编译时似乎运行得更快。 可能只是因为编译器能够比 STL 情况下更多的内联?

#include <set>
#include <queue>
#include <vector>
#include <iostream>

using namespace std;

typedef multiset<int> IntSet;

#define TIMES 10000000

void testMap()
{
    srand( 0 );

    IntSet iSet;

    for ( size_t i = 0; i < 1000; ++i ) {
        iSet.insert(rand());
    }

    for ( size_t i = 0; i < TIMES; ++i ) {
        int v = *(iSet.begin());
        iSet.erase( iSet.begin() );
        v = rand();
        iSet.insert(v);
    }
}

typedef priority_queue<int> IntQueue;

void testPriorityQueue()
{
    srand(0);
    IntQueue q;

    for ( size_t i = 0; i < 1000; ++i ) {
        q.push( rand() );
    }

    for ( size_t i = 0; i < TIMES; ++i ) {
        int v = q.top();
        q.pop();
        v = rand();
        q.push(v);
    }
}


template <class T>
class fast_priority_queue
{
public:
    fast_priority_queue()
        :size(1) {
        mVec.resize(1); // first element never used
    }
    void push( const T& rT ) {
        mVec.push_back( rT );
        size_t s = size++;
        while ( s > 1 ) {
            T* pTr = &mVec[s];
            s = s / 2;
            if ( mVec[s] > *pTr ) {
                T tmp = mVec[s];
                mVec[s] = *pTr;
                *pTr = tmp;
            } else break;
        }
    }
    const T& top() const {
        return mVec[1];
    }
    void pop() {
        mVec[1] = mVec.back();
        mVec.pop_back();
        --size;
        size_t s = 1;
        size_t n = s*2;
        T& rT = mVec[s];
        while ( n < size ) {
            if ( mVec[n] < rT ) {
                T tmp = mVec[n];
                mVec[n] = rT;
                rT = tmp;
                s = n;
                n = 2 * s;
                continue;
            }
            ++n;
            if ( mVec[n] < rT ) {
                T tmp = mVec[n];
                mVec[n] = rT;
                rT = tmp;
                s = n;
                n = 2 * s;
                continue;
            }
            break;
        }
    }
    size_t size;
    vector<T> mVec;
};

typedef fast_priority_queue<int> MyQueue;

void testMyPriorityQueue()
{
    srand(0);
    MyQueue q;

    for ( size_t i = 0; i < 1000; ++i ) {
        q.push( rand() );
    }

    for ( size_t i = 0; i < TIMES; ++i ) {
        int v = q.top();
        q.pop();
        v = rand();
        q.push(v);
    }
}


int main(int,char**)
{
    clock_t t1 = clock();
    testMyPriorityQueue();
    clock_t t2 = clock();
    testMap();
    clock_t t3 = clock();
    testPriorityQueue();
    clock_t t4 = clock();

    cout << "fast_priority_queue: " << t2 - t1 << endl;
    cout << "std::multiset: " << t3 - t2 << endl;
    cout << "std::priority_queue: " << t4 - t3 << endl;
}

当使用 g++ 4.1.2 标志编译时:64 位 Linux 上的 -O3 这给了我:

fast_priority_queue: 260000
std::multiset: 620000
std::priority_queue: 490000

【讨论】:

  • 不幸的是,您的pop() 方法不正确:向下移动新的头节点时,它必须与其最小的子节点交换。否则会立即违反堆属性。
最近更新 更多