Skip to content

Commit 0e5dbf3

Browse files
Merge pull request #6873 from simonvonhackewitz/priority_queue
Add priority queue interface with a binary heap–based implementation
2 parents def8b58 + 30779ec commit 0e5dbf3

12 files changed

Lines changed: 1602 additions & 15 deletions

File tree

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# This file is part of the Fuzion language implementation.
2+
#
3+
# The Fuzion language implementation is free software: you can redistribute it
4+
# and/or modify it under the terms of the GNU General Public License as published
5+
# by the Free Software Foundation, version 3 of the License.
6+
#
7+
# The Fuzion language implementation is distributed in the hope that it will be
8+
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
10+
# License for more details.
11+
#
12+
# You should have received a copy of the GNU General Public License along with The
13+
# Fuzion language implementation. If not, see <https://www.gnu.org/licenses/>.
14+
15+
16+
# Benchmarks for the Fuzion base module feature `container.Binary_Heap_Queue`
17+
#
18+
priority_queue_benchmark =>
19+
20+
21+
# Fill an empty queue with pseudo-random elements and then empty it
22+
#
23+
# Measure time of individual operations and show their distribution.
24+
#
25+
random_enqueue_dequeue(test_lengths Sequence i32) unit =>
26+
say """
27+
\n********************************************************************************
28+
*{"Fill empty queue with random elements, then empty it".pad_center 78}*
29+
********************************************************************************
30+
"""
31+
32+
test(size i32) unit =>
33+
34+
simple_random 42 ()->
35+
36+
m : mutate is
37+
m ! ()->
38+
39+
q := container.Binary_Heap_Queue m i32 .empty_min_first (size + 10).as_i64
40+
41+
hist_enq := time.histogram.new "Enqueue $size" nil
42+
hist_deq := time.histogram.new "Dequeue $size" nil
43+
44+
total_enq :=
45+
for t := time.duration.zero, t + dur
46+
i in 1..size
47+
do
48+
rand := random.env.next_i32
49+
dur := time.stopwatch (()->q.enqueue rand)
50+
hist_enq.add dur
51+
else
52+
t
53+
54+
total_deq :=
55+
for t := time.duration.zero, t + dur
56+
i in 1..size
57+
do
58+
dur := time.stopwatch (()->_ := q.dequeue)
59+
hist_deq.add dur
60+
else
61+
t
62+
63+
say "{hist_enq}Total time to enqueue $size elements: $total_enq\n"
64+
say "{hist_deq}Total time to dequeue $size elements: $total_deq\n"
65+
66+
for i in test_lengths do
67+
test i
68+
69+
70+
71+
72+
# Check logarithmic time complexity for enqueue and dequeue using pseudo-random elements
73+
#
74+
# Alternating addition and removal of elements is performed on queue of fixed size.
75+
# The time of the individual operations is measured and average is divided by log n,
76+
# where n is the length of the queue.
77+
# The results should then be roughly the same size, regardless of the size of the queue.
78+
#
79+
random_log_performance(test_lengths Sequence i32) unit =>
80+
say """
81+
\n********************************************************************************
82+
*{"Enqueue and dequeue performance on fixed size queue".pad_center 78}*
83+
********************************************************************************
84+
"""
85+
86+
test(queue_size i32) tuple time.histogram time.histogram =>
87+
88+
sample_size := 1E4 + 20 # histogram ignores first and last 10 samples
89+
90+
simple_random 13 ()->
91+
92+
m : mutate is
93+
m ! ()->
94+
95+
q := container.Binary_Heap_Queue m i32 .empty_min_first (queue_size + 30).as_i64
96+
97+
# fill queue
98+
for i in 1..queue_size do q.enqueue random.env.next_i32
99+
100+
hist_enq := time.histogram.new "Enqueue in queue of size $queue_size" nil 10 10
101+
hist_deq := time.histogram.new "Dequeue in queue of size $queue_size" nil 10 10
102+
103+
for i in 1..sample_size
104+
do
105+
rand := random.env.next_i32
106+
dur_enq :=time.stopwatch (()->q.enqueue rand)
107+
hist_enq.add dur_enq
108+
dur_deq :=time.stopwatch (()->_ := q.dequeue)
109+
hist_deq.add dur_deq
110+
111+
(hist_enq, hist_deq)
112+
113+
for enq_hist := Sequence (tuple i32 time.histogram) .empty, enq_hist ++ [(queue_size, enq)]
114+
deq_hist := Sequence (tuple i32 time.histogram) .empty, deq_hist ++ [(queue_size, deq)]
115+
i in test_lengths
116+
do
117+
queue_size := i
118+
enq, deq := (test i)
119+
else
120+
say "\nENQUEUE:\n"
121+
say "average"
122+
enq_hist.for_each szh->(sz,h := szh; say "{sz.as_string.pad_left 9}: {h.average.as_string_pad}")
123+
say "\naverage / log_2"
124+
enq_hist.for_each szh->(sz,h := szh; say "{sz.as_string.pad_left 9}: {h.average.times (f64.one / (sz.as_f64.log 2)) .as_string_pad}")
125+
enq_hist.for_each szh->(_ ,h := szh; say h)
126+
127+
say "\n\nDEQUEUE:\n"
128+
say "average"
129+
deq_hist.for_each szh->(sz,h := szh; say "{sz.as_string.pad_left 9}: {h.average.as_string_pad}")
130+
say "\naverage / log_2"
131+
deq_hist.for_each szh->(sz,h := szh; say "{sz.as_string.pad_left 9}: {h.average.times (f64.one / (sz.as_f64.log 2)) .as_string_pad}")
132+
deq_hist.for_each szh->(_ ,h := szh; say h)
133+
134+
135+
136+
137+
# Compare performance of make heap against iterative enqueue
138+
#
139+
# Do this for sorted, reverse-sorted, and random elements.
140+
#
141+
make_heap_vs_enqueue_all(test_lengths Sequence i32) unit =>
142+
say """
143+
\n********************************************************************************
144+
*{"Make heap vs. enqueue all".pad_center 78}*
145+
********************************************************************************
146+
"""
147+
148+
test(elements array T, elem_desc String) =>
149+
150+
sample_size := 20
151+
152+
hist_mk_heap := time.histogram.new "Make heap with {elements.length} $elem_desc elements" nil 10 0
153+
hist_enq_all := time.histogram.new "Iteratively adding {elements.length} $elem_desc elements" nil 10 0
154+
155+
for _ in 1..sample_size do
156+
m2 : mutate is
157+
m2 ! ()->
158+
hist_enq_all.add (time.stopwatch ()->(q := container.Binary_Heap_Queue m2 T .empty_min_first; q.enqueue_all (id (Sequence T) elements)))
159+
160+
m1 : mutate is
161+
m1 ! ()->
162+
hist_mk_heap.add (time.stopwatch ()->(q := container.Binary_Heap_Queue m1 T .min_first_from elements; _:=q))
163+
164+
say "{hist_mk_heap.average.as_string_pad} {hist_mk_heap.title}"
165+
say "{hist_enq_all.average.as_string_pad} {hist_enq_all.title}"
166+
say "Compared to iterative add, make heap reduces time to: {(hist_mk_heap.average.nanos.as_f64 / hist_enq_all.average.nanos.as_f64 * 100) .as_string.substring 0 4} %"
167+
168+
say "\nRandom:\n"
169+
simple_random 904644503766293287 ()->
170+
for len in test_lengths do
171+
test (array i32 .new len _->random.env.next_i32) "random"
172+
say ""
173+
174+
say "\nReverse ordered:\n"
175+
for len in test_lengths do
176+
test (1..len).reverse.as_array "reverse ordered"
177+
say ""
178+
179+
say "\nOrdered:\n"
180+
for len in test_lengths do
181+
test (1..len).as_array "ordered"
182+
say ""
183+
184+
185+
186+
187+
# Check if make heap has linear time complexity
188+
#
189+
# It seems to be dominated by some constant overhead for smaller sizes,
190+
# linear behavior shows from 1E5.
191+
#
192+
make_heap_linear_time(test_lengths Sequence i32) unit =>
193+
say """
194+
\n********************************************************************************
195+
*{"Check if make heap has linear time complexity".pad_center 78}*
196+
********************************************************************************
197+
"""
198+
199+
test(elements array T, elem_desc String) =>
200+
201+
sample_size := 20
202+
203+
hist_mk_heap := time.histogram.new "Make heap with {elements.length} $elem_desc elements" nil 10 0
204+
205+
for _ in 1..sample_size do
206+
m : mutate is
207+
m ! ()->
208+
hist_mk_heap.add (time.stopwatch ()->(q := container.Binary_Heap_Queue m T .min_first_from elements; _:=q))
209+
210+
say """
211+
{hist_mk_heap.average.times (1.0 / elements.length.as_f64) .as_string_pad} \
212+
per element for {elements.length.as_string.pad_left (test_lengths.last.or_panic.as_string.byte_count)} \
213+
$elem_desc elements. Total time was {hist_mk_heap.average.as_string_pad}"""
214+
215+
say "Random:\n"
216+
simple_random 904644503766293287 ()->
217+
for len in test_lengths do
218+
test (array i32 .new len _->random.env.next_i32) "random"
219+
220+
221+
222+
223+
# Fill queue with elements of same priority, then remove them all
224+
#
225+
# Measure time for the individual operations. They should run in O(1)
226+
# as heap property can not be violated and therefore no fixing is required
227+
#
228+
all_same_priority(test_lengths Sequence i64) unit =>
229+
say """
230+
\n********************************************************************************
231+
*{"Repeatedly enqueue identical priority then dequeue".pad_center 78}*
232+
********************************************************************************
233+
"""
234+
235+
test(queue_length i64) unit =>
236+
237+
hist_enq := time.histogram.new "Enqueue equal priority $queue_length times" nil #(time.duration.ms 5)
238+
hist_deq := time.histogram.new "Dequeue from queue with $queue_length equal elements" nil #(time.duration.ms 5)
239+
240+
m : mutate is
241+
m ! ()->
242+
243+
q := container.Binary_Heap_Queue m i32 .empty_min_first queue_length
244+
245+
for i in 1..queue_length.as_i32 do
246+
hist_enq.add (time.stopwatch ()->(q.enqueue 42))
247+
248+
for i in 1..queue_length.as_i32 do
249+
hist_deq.add (time.stopwatch ()->(ignore q.dequeue))
250+
251+
say hist_enq
252+
say hist_deq
253+
254+
for len in test_lengths do
255+
test len
256+
257+
258+
259+
# Note: large queue sizes result in considerable memory requirements:
260+
#
261+
# 1E8 * i32 (4 byte) ≈ 400 MB
262+
#
263+
# 1E9 * i32 (4 byte) ≈ 4 GB
264+
265+
random_enqueue_dequeue [1E3, 1E4, 1E5, 1E6]
266+
267+
random_log_performance [1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8]
268+
269+
make_heap_vs_enqueue_all [1E2, 1E3, 1E4, 1E5, 1E6]
270+
271+
make_heap_linear_time [1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7]
272+
273+
all_same_priority [1E2, 1E3, 1E4, 1E5, 1E6]

0 commit comments

Comments
 (0)