配对堆与std :: priority_queue

时间:2013-07-04 11:18:35

标签: c++ performance data-structures performance-testing cpu-cache

我在配对堆的C ++中实现了一个实现,我从这里开始: http://home.fnal.gov/~stoughto/build/graphviz-2.22.2/lib/vpsc/pairingheap/PairingHeap.h http://home.fnal.gov/~stoughto/build/graphviz-2.22.2/lib/vpsc/pairingheap/PairingHeap.cpp

我将PairingHeap与std :: priority_queue进行了比较 这些是结果:

gcc 4.7 -O3,核心i7 2.4Ghz 用于测量周期的rdstc指令

-------------------------------------------------------------------------------

for 100.000 elements:
o std::priority_queue<int>
    - insert:           9,800,415 cycles
    - extract:         29,712,818 cycles
    - total:           39,513,233 cycles       [0.031secs]
o PairingHeap<int>
    - insert:          34,381,467 cycles
    - extract:        259,986,113 cycles
    - total:          294,367,580 cycles       [0.125secs]


-------------------------------------------------------------------------------


for 1.000.000 elements:
o std::priority_queue<int>
    - insert:         95,954,533 cycles
    - extract:       518,546,747 cycles
    - total:         614,501,280 cycles       [0.296secs]
o PairingHeap<int>
    - insert:        344,453,782 cycles
    - extract:     3,856,344,199 cycles
    - total:       4,200,797,981 cycles       [1.593secs]

-------------------------------------------------------------------------------


for 10.000.000 elements:
o std::priority_queue<int>
    - insert:        999,836,450 cycles
    - extract:    10,634,407,049 cycles
    - total:      11,634,243,499 cycles       [4.390secs]
o PairingHeap<int>
    - insert:      3,441,903,781 cycles
    - extract:    61,166,421,272 cycles
    - total:      64,608,325,053 cycles       [24.187secs]

配对堆应该比std :: priority_queue更快,因为它应该渐近更快 操作,但在这里,配对堆非常更慢。 我认为这是因为std :: priority_queue在引擎盖下使用了一个向量,这就更多了 缓存友好比为每个整数分配节点,如配对堆那样。

所以,我的问题是:渐近更好的数据结构可以(很大程度上)被更糟糕的数据结构击败, 只是因为它们更适合缓存? 是否真的值得在更复杂的数据结构中花费大量时间,例如配对堆,何时 默认的std :: priority_queue可能比它快吗?

我只是没有考虑到我使用的配对堆的实现只是废话, 但它似乎不是,因为我尝试过的其他实现更糟糕! 想法?

2 个答案:

答案 0 :(得分:4)

  

所以,我的问题是:渐近更好的数据结构能否(大部分)被更糟糕的数据结构击败,只是因为它们对缓存更友好了吗?

是的,这种情况一直都在发生。除了缓存友好性之外还有其他原因(常数因素)。与同一个单词的其他用法一样,渐近在这里指的是某些(通常是问题大小)转到无穷大。渐近地优于B的只是说它最终更好,而不是对于某个给定值它会更好(甚至相等)。请注意,对于较大的数据集,该比率确实有所改善,但还不够。

请注意,即使二进制堆对于某些大型数据集(例如您的数据集)也不太容易缓存。 节点的子节点和父节点可能位于一个完全不同的页面上,因此您只能在最后几个级别从缓存中获取一些内容(或者如果您访问的元素恰好具有相似的路径,但这几乎是一个给定的任何数据结构)。 有一个名为B-heap的变种可以对此进行改进,但是我还没有找到很多细节(只有两个实现和一个关于RAM计算模型如何误导的咆哮)。

你必须要确定,但重复分配和释放可能需要花费大量时间。池分配器(boost,或std :: vector顶部的手动滚动 - 允许用整数替换指针,这可能节省一些空间)可以大大降低此成本。 该实现似乎也使用子列表的链接列表,这可能会更多地损害缓存。阵列需要一些额外的副本,但在实践中可能是一种改进。

  

当默认的std :: priority_queue可以比它快得多时,是否真的值得花更多时间在更复杂的数据结构(如配对堆)上?

有足够大的数据集与一些优化(例如专门的分配器和聪明的节点布局)相结合可能会使平衡有利于它。 在任何情况下,这个语句有点弄巧成拙:如果配对堆比预期用例的二进制堆快,那么标准库可能会使用配对堆!

此外,至少在纯函数式语言中,配对堆实现起来非常简单(尽管效率不高)。这对你和C ++来说可能没有多大用处,但这对“更复杂”的部分来说是挑战和挑战。

答案 1 :(得分:1)

这里的一个主要问题是内存分配和是缓存效率。

您可以尝试为operator new类实现具有自定义operator delete + PairNode的固定大小分配器,以减少分配开销(类似于“更有效的C ++”中的分配开销) “,第10项)。此外,这种方法最终可能更加缓存,因为元素更可能具有引用的位置。

我之前使用QuadEdge结构(遭遇类似问题)对Delaunay三角测量进行了此操作,并且速度增加超过10-20x IIRC。如果你需要让分配器线程安全,那么你将在性能方面付出高昂的代价。

至于实际回答1或者其他情况下表现是否更好的问题,它不太可能是普遍的,并且逐案分析是最简单的知道方式(任何其他方法都会因为如果不实施它就无法预测实施的质量,因此很复杂)。不仅如此,不同的处理器也会有所不同,结果可能取决于您倾向于获得的数据。