Skip to main content

libxev 源码阅读 - Pairing Heap

·362 words·2 mins
Table of Contents

Pairing Heap 是 libxev 中用到的一个数据结构,用于 Timer 的维护。因为在事件循环中,我们需要获取即将/已经超时的定时器,所以需要一种数据结构提供极快的 getMindeleteMin 操作。最小堆正式符合这一特点的结构。更常见的数据结构是使用四叉堆,比如 Golang。

Quaternary Heap
#

二叉堆和四叉堆都属于d-ary-heap,可以使用数组来存储

关于二叉堆和四叉堆的性能比较可以参考

Pairing-Heap
#

以最小堆为例,配对堆是一棵满足堆性质的带权多叉树,每个节点的值都小于或等于他的所有的子节点

在基于指针的实现中,为了支持随机存取存储器(RAM)上的机器操作,以及减少键值操作,每个节点可以使用三个指针来实现。这通过将节点的子节点表示为一个双向链表实现:

  • 一个指针指向节点的第一个子节点
  • 一个指针指向其下一个兄弟节点
  • 一个指针指向其上一个兄弟节点(对于最左边的兄弟节点,该指针指向其父节点)

这种结构也可以看作是“左子树-右兄弟”(Left-child right-sibling)二叉树的一种变体

Insert
#

将新的元素视为一个堆和原堆进行合并。令两个根节点较小的一个为新的根节点,然后将较大的根节点作为它的子节点插入进去

需要注意插入操作是没有按照值的大小对子节点进行维护的。但是最右边的节点是最早的子节点,最左边的节点是最近的子节点。

deleteMin
#

  1. 根节点为最小节点,直接移除
  2. 将子节点两两进行一次合并操作
  3. 将新产生的堆 从右往左,逐个合并

为什么需要两遍合并,可以参考 Why does pairing heap need that special two passes when delete_min?

配对堆具体的实现代码可以参考 https://github.com/mitchellh/libxev/blob/db6a52bafadf00360e675fefa7926e8e6c0e9931/src/heap.zig#L24

Benchmark
#

参考 wiki ,各种堆的时间复杂度为

Heap TypeFind MinDelete MinDecrease KeyInsertMeldMake Heap
BinaryΘ(1)Θ(log n)Θ(log n)Θ(log n)Θ(n)Θ(n)
SkewΘ(1)O(log n) am.O(log n) am.O(log n) am.O(log n) am.Θ(n) am.
LeftistΘ(1)Θ(log n)Θ(log n)Θ(log n)Θ(log n)Θ(n)
BinomialΘ(1)Θ(log n)Θ(log n)Θ(1) am.Θ(log n)Θ(n)
Skew BinomialΘ(1)Θ(log n)Θ(log n)Θ(1)Θ(log n)Θ(n)
2–3 HeapΘ(1)O(log n) am.Θ(1)Θ(1) am.O(log n)Θ(n)
Bottom-up SkewΘ(1)O(log n) am.O(log n) am.Θ(1) am.Θ(1) am.Θ(n) am.
PairingΘ(1)O(log n) am.o(log n) am.Θ(1)Θ(1)Θ(n)
Rank-pairingΘ(1)O(log n) am.Θ(1) am.Θ(1)Θ(1)Θ(n)
FibonacciΘ(1)O(log n) am.Θ(1) am.Θ(1)Θ(1)Θ(n)
Strict FibonacciΘ(1)Θ(log n)Θ(1)Θ(1)Θ(1)Θ(n)
BrodalΘ(1)Θ(log n)Θ(1)Θ(1)Θ(1)Θ(n)

deleteMin 操作时,虽然 Pairing Heap 的时间复杂度为 o(log n) am;二叉堆的时间复杂度为 Θ(log n),看起来 Pairing Heap 是更优的。但是实际测试下来二叉堆的耗时更少一点,参考 https://github.com/Hanaasagi/heap-benchmark。大概是因为 Pairing Heap 存在缓存命中问题。所以其实 Big O 这种还是要结合具体的计算机架构来讨论才具有实际意义,比如 HashMap 是 O(1),但是在一些场景是不如直接遍历一个数组的

  Operating System: linux x86_64
  CPU:              AMD Ryzen 7 5800H with Radeon Graphics
  CPU Cores:        8
  Total Memory:     27.271GiB


benchmark              runs     total time     time/run (avg ± σ)     (min ... max)                p75        p99        p995
-----------------------------------------------------------------------------------------------------------------------------
B_HeapInsert           8191     1.499s         183.021us ± 31.67us    (166.5us ... 814.134us)      185.147us  270.284us  476.942us
B_HeapInsertWithAlloc  8191     1.541s         188.167us ± 29.909us   (170.621us ... 750.858us)    190.316us  241.998us  462.346us
B_HeapDeleteMin        2047     1.228s         600.258us ± 33.905us   (563.894us ... 1.105ms)      610.198us  762.102us  788.432us
Q_HeapInsert           8191     1.22s          149.062us ± 30.254us   (133.326us ... 752.255us)    150.297us  187.173us  440.486us
Q_HeapInsertWithAlloc  8191     1.225s         149.659us ± 27.223us   (135.073us ... 760.078us)    151.624us  184.589us  339.706us
Q_HeapDeleteMin        2047     1.108s         541.296us ± 29.77us    (508.65us ... 798.35us)      554.117us  679.271us  704.693us
P_HeapInsert           65535    950.686ms      14.506us ± 8.855us     (11.943us ... 966.388us)     14.387us   24.305us   27.168us
P_HeapDeleteMin        2047     1.908s         932.325us ± 44.969us   (870.776us ... 1.382ms)      950.813us  1.116ms    1.156ms

Reference
#

Related

libxev 源码阅读 - MPSC Queue
·1239 words·6 mins
Spice 源码剖析
·1871 words·9 mins