Skip to content

Commit 6928ca7

Browse files
ysbaddadenyxhuvudstraight-shoota
authored
EventLoop: store Timers in min Pairing Heap [fixup #14996] (#15206)
Related to [RFC #12](crystal-lang/rfcs#12). Replaces the `Deque` used in #14996 for a min [Pairing Heap] which is a kind of [Mergeable Heap] and is one of the best performing heap in practical tests when arbitrary deletions are required (think cancelling a timeout), otherwise a D-ary Heap (e.g. 4-heap) will usually perform better. See the [A Nearly-Tight Analysis of Multipass Pairing Heaps](https://epubs.siam.org/doi/epdf/10.1137/1.9781611973068.52) paper or the Wikipedia page for more details. The implementation itself is based on the [Pairing Heaps: Experiments and Analysis](https://dl.acm.org/doi/pdf/10.1145/214748.214759) paper, and merely implements a recursive twopass algorithm (the auxiliary twopass might perform even better). The `Crystal::PointerPairingList(T)` type is generic and relies on intrusive nodes (the links are into `T`) to avoid extra allocations for the nodes (same as `Crystal::PointerLinkedList(T)`). It also requires a `T#heap_compare` method, so we can use the same type for a min or max heap, or to build a more complex comparison. Note: I also tried a 4-heap, and while it performs very well and only needs a flat array, the arbitrary deletion (e.g. cancelling timeout) needs a linear scan and its performance quickly plummets, even at low occupancy, and becomes painfully slow at higher occupancy (tens of microseconds on _each_ delete, while the pairing heap does it in tens of nanoseconds). Follow up to #14996 [Mergeable Heap]: https://en.wikipedia.org/wiki/Mergeable_heap [Pairing Heap]: https://en.wikipedia.org/wiki/Pairing_heap [D-ary Heap]: https://en.wikipedia.org/wiki/D-ary_heap Co-authored-by: Linus Sellberg <[email protected]> Co-authored-by: Johannes Müller <[email protected]>
1 parent ec11b2d commit 6928ca7

File tree

5 files changed

+349
-59
lines changed

5 files changed

+349
-59
lines changed

spec/std/crystal/evented/timers_spec.cr

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ describe Crystal::Evented::Timers do
1010
event = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 7.seconds)
1111
timers.add(pointerof(event))
1212
timers.empty?.should be_false
13+
14+
timers.delete(pointerof(event))
15+
timers.empty?.should be_true
1316
end
1417

1518
it "#next_ready?" do
@@ -18,9 +21,18 @@ describe Crystal::Evented::Timers do
1821
timers.next_ready?.should be_nil
1922

2023
# with events
21-
event = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 5.seconds)
22-
timers.add(pointerof(event))
23-
timers.next_ready?.should eq(event.wake_at?)
24+
event1s = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 1.second)
25+
event3m = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 3.minutes)
26+
event5m = Crystal::Evented::Event.new(:sleep, Fiber.current, timeout: 5.minutes)
27+
28+
timers.add(pointerof(event5m))
29+
timers.next_ready?.should eq(event5m.wake_at?)
30+
31+
timers.add(pointerof(event1s))
32+
timers.next_ready?.should eq(event1s.wake_at?)
33+
34+
timers.add(pointerof(event3m))
35+
timers.next_ready?.should eq(event1s.wake_at?)
2436
end
2537

2638
it "#dequeue_ready" do
@@ -66,16 +78,6 @@ describe Crystal::Evented::Timers do
6678

6779
event0.wake_at = -1.minute
6880
timers.add(pointerof(event0)).should be_true # added new head (next ready)
69-
70-
events = [] of Crystal::Evented::Event*
71-
timers.each { |event| events << event }
72-
events.should eq([
73-
pointerof(event0),
74-
pointerof(event1),
75-
pointerof(event3),
76-
pointerof(event2),
77-
])
78-
timers.empty?.should be_false
7981
end
8082

8183
it "#delete" do
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
require "spec"
2+
require "../../../src/crystal/pointer_pairing_heap"
3+
4+
private struct Node
5+
getter key : Int32
6+
7+
include Crystal::PointerPairingHeap::Node
8+
9+
def initialize(@key : Int32)
10+
end
11+
12+
def heap_compare(other : Pointer(self)) : Bool
13+
key < other.value.key
14+
end
15+
16+
def inspect(io : IO, indent = 0) : Nil
17+
prv = @heap_previous
18+
nxt = @heap_next
19+
chd = @heap_child
20+
21+
indent.times { io << ' ' }
22+
io << "Node value=" << key
23+
io << " prv=" << prv.try(&.value.key)
24+
io << " nxt=" << nxt.try(&.value.key)
25+
io << " chd=" << chd.try(&.value.key)
26+
io.puts
27+
28+
node = heap_child?
29+
while node
30+
node.value.inspect(io, indent + 2)
31+
node = node.value.heap_next?
32+
end
33+
end
34+
end
35+
36+
describe Crystal::PointerPairingHeap do
37+
it "#add" do
38+
heap = Crystal::PointerPairingHeap(Node).new
39+
node1 = Node.new(1)
40+
node2 = Node.new(2)
41+
node2b = Node.new(2)
42+
node3 = Node.new(3)
43+
44+
# can add distinct nodes
45+
heap.add(pointerof(node3))
46+
heap.add(pointerof(node1))
47+
heap.add(pointerof(node2))
48+
49+
# can add duplicate key (different nodes)
50+
heap.add(pointerof(node2b))
51+
52+
# can't add same node twice
53+
expect_raises(ArgumentError) { heap.add(pointerof(node1)) }
54+
55+
# can re-add removed nodes
56+
heap.delete(pointerof(node3))
57+
heap.add(pointerof(node3))
58+
59+
heap.shift?.should eq(pointerof(node1))
60+
heap.add(pointerof(node1))
61+
end
62+
63+
it "#shift?" do
64+
heap = Crystal::PointerPairingHeap(Node).new
65+
nodes = StaticArray(Node, 10).new { |i| Node.new(i) }
66+
67+
# insert in random order
68+
(0..9).to_a.shuffle.each do |i|
69+
heap.add nodes.to_unsafe + i
70+
end
71+
72+
# removes in ascending order
73+
10.times do |i|
74+
node = heap.shift?
75+
node.should eq(nodes.to_unsafe + i)
76+
end
77+
end
78+
79+
it "#delete" do
80+
heap = Crystal::PointerPairingHeap(Node).new
81+
nodes = StaticArray(Node, 10).new { |i| Node.new(i) }
82+
83+
# insert in random order
84+
(0..9).to_a.shuffle.each do |i|
85+
heap.add nodes.to_unsafe + i
86+
end
87+
88+
# remove some values
89+
heap.delete(nodes.to_unsafe + 3)
90+
heap.delete(nodes.to_unsafe + 7)
91+
heap.delete(nodes.to_unsafe + 1)
92+
93+
# remove tail
94+
heap.delete(nodes.to_unsafe + 9)
95+
96+
# remove head
97+
heap.delete(nodes.to_unsafe + 0)
98+
99+
# repeatedly delete min
100+
[2, 4, 5, 6, 8].each do |i|
101+
heap.shift?.should eq(nodes.to_unsafe + i)
102+
end
103+
heap.shift?.should be_nil
104+
end
105+
106+
it "adds 1000 nodes then shifts them in order" do
107+
heap = Crystal::PointerPairingHeap(Node).new
108+
109+
nodes = StaticArray(Node, 1000).new { |i| Node.new(i) }
110+
(0..999).to_a.shuffle.each { |i| heap.add(nodes.to_unsafe + i) }
111+
112+
i = 0
113+
while node = heap.shift?
114+
node.value.key.should eq(i)
115+
i += 1
116+
end
117+
i.should eq(1000)
118+
119+
heap.shift?.should be_nil
120+
end
121+
122+
it "randomly shift while we add nodes" do
123+
heap = Crystal::PointerPairingHeap(Node).new
124+
125+
nodes = uninitialized StaticArray(Node, 1000)
126+
(0..999).to_a.shuffle.each_with_index { |i, j| nodes[j] = Node.new(i) }
127+
128+
i = 0
129+
removed = 0
130+
131+
# regularly calls delete-min while we insert
132+
loop do
133+
if rand(0..5) == 0
134+
removed += 1 if heap.shift?
135+
else
136+
heap.add(nodes.to_unsafe + i)
137+
break if (i += 1) == 1000
138+
end
139+
end
140+
141+
# exhaust the heap
142+
while heap.shift?
143+
removed += 1
144+
end
145+
146+
# we must have added and removed all nodes _once_
147+
i.should eq(1000)
148+
removed.should eq(1000)
149+
end
150+
end
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# :nodoc:
2+
#
3+
# Tree of `T` structs referenced as pointers.
4+
# `T` must include `Crystal::PointerPairingHeap::Node`.
5+
class Crystal::PointerPairingHeap(T)
6+
module Node
7+
macro included
8+
property? heap_previous : Pointer(self)?
9+
property? heap_next : Pointer(self)?
10+
property? heap_child : Pointer(self)?
11+
end
12+
13+
# Compare self with other. For example:
14+
#
15+
# Use `<` to create a min heap.
16+
# Use `>` to create a max heap.
17+
abstract def heap_compare(other : Pointer(self)) : Bool
18+
end
19+
20+
@head : Pointer(T)?
21+
22+
private def head=(head)
23+
@head = head
24+
head.value.heap_previous = nil if head
25+
head
26+
end
27+
28+
def empty?
29+
@head.nil?
30+
end
31+
32+
def first? : Pointer(T)?
33+
@head
34+
end
35+
36+
def shift? : Pointer(T)?
37+
if node = @head
38+
self.head = merge_pairs(node.value.heap_child?)
39+
node.value.heap_child = nil
40+
node
41+
end
42+
end
43+
44+
def add(node : Pointer(T)) : Nil
45+
if node.value.heap_previous? || node.value.heap_next? || node.value.heap_child?
46+
raise ArgumentError.new("The node is already in a Pairing Heap tree")
47+
end
48+
self.head = meld(@head, node)
49+
end
50+
51+
def delete(node : Pointer(T)) : Nil
52+
if previous_node = node.value.heap_previous?
53+
next_sibling = node.value.heap_next?
54+
55+
if previous_node.value.heap_next? == node
56+
previous_node.value.heap_next = next_sibling
57+
else
58+
previous_node.value.heap_child = next_sibling
59+
end
60+
61+
if next_sibling
62+
next_sibling.value.heap_previous = previous_node
63+
end
64+
65+
subtree = merge_pairs(node.value.heap_child?)
66+
clear(node)
67+
self.head = meld(@head, subtree)
68+
else
69+
# removing head
70+
self.head = merge_pairs(node.value.heap_child?)
71+
node.value.heap_child = nil
72+
end
73+
end
74+
75+
def clear : Nil
76+
if node = @head
77+
clear_recursive(node)
78+
@head = nil
79+
end
80+
end
81+
82+
private def clear_recursive(node)
83+
child = node.value.heap_child?
84+
while child
85+
clear_recursive(child)
86+
child = child.value.heap_next?
87+
end
88+
clear(node)
89+
end
90+
91+
private def meld(a : Pointer(T), b : Pointer(T)) : Pointer(T)
92+
if a.value.heap_compare(b)
93+
add_child(a, b)
94+
else
95+
add_child(b, a)
96+
end
97+
end
98+
99+
private def meld(a : Pointer(T), b : Nil) : Pointer(T)
100+
a
101+
end
102+
103+
private def meld(a : Nil, b : Pointer(T)) : Pointer(T)
104+
b
105+
end
106+
107+
private def meld(a : Nil, b : Nil) : Nil
108+
end
109+
110+
private def add_child(parent : Pointer(T), node : Pointer(T)) : Pointer(T)
111+
first_child = parent.value.heap_child?
112+
parent.value.heap_child = node
113+
114+
first_child.value.heap_previous = node if first_child
115+
node.value.heap_previous = parent
116+
node.value.heap_next = first_child
117+
118+
parent
119+
end
120+
121+
private def merge_pairs(node : Pointer(T)?) : Pointer(T)?
122+
return unless node
123+
124+
# 1st pass: meld children into pairs (left to right)
125+
tail = nil
126+
127+
while a = node
128+
if b = a.value.heap_next?
129+
node = b.value.heap_next?
130+
root = meld(a, b)
131+
root.value.heap_previous = tail
132+
tail = root
133+
else
134+
a.value.heap_previous = tail
135+
tail = a
136+
break
137+
end
138+
end
139+
140+
# 2nd pass: meld the pairs back into a single tree (right to left)
141+
root = nil
142+
143+
while tail
144+
node = tail.value.heap_previous?
145+
root = meld(root, tail)
146+
tail = node
147+
end
148+
149+
root.value.heap_next = nil if root
150+
root
151+
end
152+
153+
private def clear(node) : Nil
154+
node.value.heap_previous = nil
155+
node.value.heap_next = nil
156+
node.value.heap_child = nil
157+
end
158+
end

src/crystal/system/unix/evented/event.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "crystal/pointer_linked_list"
2+
require "crystal/pointer_pairing_heap"
23

34
# Information about the event that a `Fiber` is waiting on.
45
#
@@ -35,6 +36,9 @@ struct Crystal::Evented::Event
3536
# The event can be added to `Waiters` lists.
3637
include PointerLinkedList::Node
3738

39+
# The event can be added to the `Timers` list.
40+
include PointerPairingHeap::Node
41+
3842
def initialize(@type : Type, @fiber, @index = nil, timeout : Time::Span? = nil)
3943
if timeout
4044
seconds, nanoseconds = System::Time.monotonic
@@ -55,4 +59,8 @@ struct Crystal::Evented::Event
5559
# NOTE: musn't be changed after registering the event into `Timers`!
5660
def wake_at=(@wake_at)
5761
end
62+
63+
def heap_compare(other : Pointer(self)) : Bool
64+
wake_at < other.value.wake_at
65+
end
5866
end

0 commit comments

Comments
 (0)