d_heap 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +30 -1
- data/CHANGELOG.md +42 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -10
- data/README.md +353 -121
- data/benchmarks/push_n.yml +28 -0
- data/benchmarks/push_n_pop_n.yml +31 -0
- data/benchmarks/push_pop.yml +24 -0
- data/bin/bench_n +7 -0
- data/bin/benchmark-driver +29 -0
- data/bin/benchmarks +10 -0
- data/bin/profile +10 -0
- data/d_heap.gemspec +2 -1
- data/docs/benchmarks-2.txt +52 -0
- data/docs/benchmarks.txt +443 -0
- data/docs/profile.txt +392 -0
- data/ext/d_heap/d_heap.c +428 -150
- data/ext/d_heap/d_heap.h +6 -3
- data/ext/d_heap/extconf.rb +8 -3
- data/lib/benchmark_driver/runner/ips_zero_fail.rb +120 -0
- data/lib/d_heap.rb +5 -3
- data/lib/d_heap/benchmarks.rb +111 -0
- data/lib/d_heap/benchmarks/benchmarker.rb +113 -0
- data/lib/d_heap/benchmarks/implementations.rb +168 -0
- data/lib/d_heap/benchmarks/profiler.rb +71 -0
- data/lib/d_heap/benchmarks/rspec_matchers.rb +374 -0
- data/lib/d_heap/version.rb +1 -1
- metadata +34 -3
data/ext/d_heap/d_heap.h
CHANGED
@@ -11,6 +11,12 @@
|
|
11
11
|
// comparisons as d gets further from 4.
|
12
12
|
#define DHEAP_MAX_D 32
|
13
13
|
|
14
|
+
#define DHEAP_DEFAULT_SIZE 16
|
15
|
+
#define DHEAP_MAX_SIZE (LONG_MAX / (int)sizeof(long double))
|
16
|
+
|
17
|
+
// 10MB
|
18
|
+
#define DHEAP_CAPA_INCR_MAX (10 * 1024 * 1024 / (int)sizeof(long double))
|
19
|
+
|
14
20
|
VALUE rb_cDHeap;
|
15
21
|
|
16
22
|
// copied from pg gem
|
@@ -26,9 +32,6 @@ VALUE rb_cDHeap;
|
|
26
32
|
#define dheap_gc_location(x) UNUSED(x)
|
27
33
|
#endif
|
28
34
|
|
29
|
-
// from internal/compar.h
|
30
|
-
#define STRING_P(s) (RB_TYPE_P((s), T_STRING) && CLASS_OF(s) == rb_cString)
|
31
|
-
|
32
35
|
#ifdef __D_HEAP_DEBUG
|
33
36
|
#define debug(v) { \
|
34
37
|
ID sym_puts = rb_intern("puts"); \
|
data/ext/d_heap/extconf.rb
CHANGED
@@ -2,10 +2,15 @@
|
|
2
2
|
|
3
3
|
require "mkmf"
|
4
4
|
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# end
|
5
|
+
# For testing in CI (because I don't otherwise have easy access to Mac OS):
|
6
|
+
# $CFLAGS << " -D__D_HEAP_DEBUG" if /darwin/ =~ RUBY_PLATFORM
|
8
7
|
|
9
8
|
have_func "rb_gc_mark_movable" # since ruby-2.7
|
10
9
|
|
10
|
+
check_sizeof("long")
|
11
|
+
check_sizeof("unsigned long long")
|
12
|
+
check_sizeof("long double")
|
13
|
+
have_macro("LDBL_MANT_DIG", "float.h")
|
14
|
+
|
15
|
+
CONFIG["warnflags"] << " -Werror"
|
11
16
|
create_makefile("d_heap/d_heap")
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English" # $CHILD_STATUS
|
4
|
+
require "timeout" # Timeout::Error
|
5
|
+
|
6
|
+
require "benchmark_driver"
|
7
|
+
|
8
|
+
# monkey-patch to convert miniscule values to 0.0
|
9
|
+
class BenchmarkDriver::Output::Compare
|
10
|
+
|
11
|
+
# monkey-patch to convert miniscule values to 0.0
|
12
|
+
module MinisculeToZero
|
13
|
+
|
14
|
+
def humanize(value, width = 10)
|
15
|
+
value <= 0.0.next_float.next_float ? 0.0 : super(value, width)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
prepend MinisculeToZero
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# A simple patch to let slow specs error out without
|
25
|
+
class BenchmarkDriver::Runner::IpsZeroFail < BenchmarkDriver::Runner::Ips
|
26
|
+
METRIC = BenchmarkDriver::Runner::Ips::METRIC
|
27
|
+
|
28
|
+
# always run at least once
|
29
|
+
class Job < BenchmarkDriver::DefaultJob
|
30
|
+
attr_accessor :warmup_value, :warmup_duration, :warmup_loop_count
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# BenchmarkDriver::Runner looks for this class
|
35
|
+
JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
|
36
|
+
|
37
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
38
|
+
|
39
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
40
|
+
# @param [Array<BenchmarkDriver::Default::Job>] jobs
|
41
|
+
def run(jobs)
|
42
|
+
if jobs.any? { |job| job.loop_count.nil? }
|
43
|
+
@output.with_warmup do
|
44
|
+
jobs = jobs.map do |job|
|
45
|
+
next job if job.loop_count # skip warmup if loop_count is set
|
46
|
+
|
47
|
+
@output.with_job(name: job.name) do
|
48
|
+
context = job.runnable_contexts(@contexts).first
|
49
|
+
duration, loop_count = run_warmup(job, context: context)
|
50
|
+
value, duration = value_duration(duration: duration, loop_count: loop_count)
|
51
|
+
|
52
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
53
|
+
@output.report(values: { metric => value }, duration: duration, loop_count: loop_count)
|
54
|
+
end
|
55
|
+
|
56
|
+
warmup_loop_count = loop_count
|
57
|
+
|
58
|
+
loop_count = (loop_count.to_f * @config.run_duration / duration).floor
|
59
|
+
Job.new(**job.to_h.merge(loop_count: loop_count))
|
60
|
+
.tap {|j| j.warmup_value = value }
|
61
|
+
.tap {|j| j.warmup_duration = duration }
|
62
|
+
.tap {|j| j.warmup_loop_count = warmup_loop_count }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
.compact
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@output.with_benchmark do
|
70
|
+
jobs.each do |job|
|
71
|
+
@output.with_job(name: job.name) do
|
72
|
+
job.runnable_contexts(@contexts).each do |context|
|
73
|
+
repeat_params = { config: @config, larger_better: true, rest_on_average: :average }
|
74
|
+
result =
|
75
|
+
if job.loop_count&.positive?
|
76
|
+
loop_count = job.loop_count
|
77
|
+
BenchmarkDriver::Repeater.with_repeat(**repeat_params) do
|
78
|
+
run_benchmark(job, context: context)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
loop_count = job.warmup_loop_count
|
82
|
+
repeater_value = [job.warmup_value, job.warmup_duration]
|
83
|
+
BenchmarkDriver::Repeater::RepeatResult.new(
|
84
|
+
value: repeater_value, all_values: [repeater_value]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
value, duration = result.value
|
88
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
89
|
+
@output.report(
|
90
|
+
values: { metric => value },
|
91
|
+
all_values: { metric => result.all_values },
|
92
|
+
duration: duration,
|
93
|
+
loop_count: loop_count,
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
103
|
+
|
104
|
+
def run_warmup(job, context:)
|
105
|
+
start = Time.now
|
106
|
+
super(job, context: context)
|
107
|
+
rescue Timeout::Error
|
108
|
+
[Time.now - start, 0.0.next_float]
|
109
|
+
end
|
110
|
+
|
111
|
+
def execute(*args, exception: true)
|
112
|
+
super
|
113
|
+
rescue RuntimeError => ex
|
114
|
+
if args.include?("timeout") && $CHILD_STATUS&.exitstatus == 124
|
115
|
+
raise Timeout::Error, ex.message
|
116
|
+
end
|
117
|
+
raise ex
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/lib/d_heap.rb
CHANGED
@@ -14,9 +14,11 @@ require "d_heap/version"
|
|
14
14
|
# worst-case time complexity.
|
15
15
|
#
|
16
16
|
class DHeap
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
# ruby 3.0+ (2.x can just use inherited initialize_clone)
|
18
|
+
if Object.instance_method(:initialize_clone).arity == -1
|
19
|
+
def initialize_clone(other, freeze: nil)
|
20
|
+
__init_clone__(other, freeze ? true : freeze)
|
21
|
+
end
|
20
22
|
end
|
21
23
|
|
22
24
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
# Different benchmark scenarios and implementations to benchmark
|
7
|
+
module DHeap::Benchmarks
|
8
|
+
|
9
|
+
def self.puts_version_info(type = "Benchmark", io = $stdout)
|
10
|
+
io.puts "#{type} run at %s" % [Time.now]
|
11
|
+
io.puts "ruby v%s, DHeap v%s" % [RUBY_VERSION, DHeap::VERSION]
|
12
|
+
io.puts
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Style/NumericPredicate
|
16
|
+
|
17
|
+
# moves "rand" outside the benchmarked code, to avoid measuring that too.
|
18
|
+
module Randomness
|
19
|
+
|
20
|
+
def default_randomness_size; 1_000_000 end
|
21
|
+
|
22
|
+
def fill_random_vals(target_size = default_randomness_size, io: $stdout)
|
23
|
+
@dheap_bm_random_vals ||= []
|
24
|
+
count = target_size - @dheap_bm_random_vals.length
|
25
|
+
return 0 if count <= 0
|
26
|
+
millions = (count / 1_000_000.0).round(3)
|
27
|
+
io&.puts "~~~~~~ filling @dheap_bm_random_vals with #{millions}M ~~~~~~"
|
28
|
+
io&.flush
|
29
|
+
count.times do @dheap_bm_random_vals << rand(0..10_000) end
|
30
|
+
@dheap_bm_random_len = @dheap_bm_random_vals.length
|
31
|
+
@dheap_bm_random_idx = (((@dheap_bm_random_idx || -1) + 1) % @dheap_bm_random_len)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def random_val
|
36
|
+
@dheap_bm_random_vals.fetch(
|
37
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# different scenarios to be benchmarked or profiled
|
44
|
+
module Scenarios
|
45
|
+
|
46
|
+
def push_n_multiple_queues(count, *queues)
|
47
|
+
while 0 < count
|
48
|
+
value = @dheap_bm_random_vals.fetch(
|
49
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
50
|
+
)
|
51
|
+
queues.each do |queue|
|
52
|
+
queue << value
|
53
|
+
end
|
54
|
+
count -= 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def push_n(queue, count)
|
59
|
+
while 0 < count
|
60
|
+
queue << @dheap_bm_random_vals.fetch(
|
61
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
62
|
+
)
|
63
|
+
count -= 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def push_n_then_pop_n(queue, count) # rubocop:disable Metrics/MethodLength
|
68
|
+
i = 0
|
69
|
+
while i < count
|
70
|
+
queue << @dheap_bm_random_vals.fetch(
|
71
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
72
|
+
)
|
73
|
+
i += 1
|
74
|
+
end
|
75
|
+
while 0 < i
|
76
|
+
queue.pop
|
77
|
+
i -= 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def repeated_push_pop(queue, count)
|
82
|
+
while 0 < count
|
83
|
+
queue << @dheap_bm_random_vals.fetch(
|
84
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
85
|
+
)
|
86
|
+
queue.pop
|
87
|
+
count -= 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
include Randomness
|
94
|
+
include Scenarios
|
95
|
+
|
96
|
+
def initq(klass, count = 0)
|
97
|
+
queue = klass.new
|
98
|
+
while 0 < count
|
99
|
+
queue << @dheap_bm_random_vals.fetch(
|
100
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
101
|
+
)
|
102
|
+
count -= 1
|
103
|
+
end
|
104
|
+
queue
|
105
|
+
end
|
106
|
+
|
107
|
+
# rubocop:enable Style/NumericPredicate
|
108
|
+
|
109
|
+
require "d_heap/benchmarks/implementations"
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap/benchmarks"
|
4
|
+
|
5
|
+
require "benchmark_driver"
|
6
|
+
require "shellwords"
|
7
|
+
require "English"
|
8
|
+
|
9
|
+
module DHeap::Benchmarks
|
10
|
+
# Benchmarks different implementations with different sizes
|
11
|
+
class Benchmarker
|
12
|
+
include Randomness
|
13
|
+
include Scenarios
|
14
|
+
|
15
|
+
N_COUNTS = [
|
16
|
+
5, # 1 + 4
|
17
|
+
21, # 1 + 4 + 16
|
18
|
+
85, # 1 + 4 + 16 + 64
|
19
|
+
341, # 1 + 4 + 16 + 64 + 256
|
20
|
+
1365, # 1 + 4 + 16 + 64 + 256 + 1024
|
21
|
+
5461, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096
|
22
|
+
21_845, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384
|
23
|
+
87_381, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384 + 65536
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
attr_reader :time
|
27
|
+
attr_reader :iterations_for_push_pop
|
28
|
+
attr_reader :io
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
time: Integer(ENV.fetch("BENCHMARK_TIME", 10)),
|
32
|
+
iterations_for_push_pop: 10_000,
|
33
|
+
io: $stdout
|
34
|
+
)
|
35
|
+
@time = time
|
36
|
+
@iterations_for_push_pop = Integer(iterations_for_push_pop)
|
37
|
+
@io = io
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(queue_size: ENV.fetch("BENCHMARK_QUEUE_SIZE", :unset))
|
41
|
+
DHeap::Benchmarks.puts_version_info("Benchmarking")
|
42
|
+
sizes = (queue_size == :unset) ? N_COUNTS : [Integer(queue_size)]
|
43
|
+
sizes.each do |size|
|
44
|
+
benchmark_size(size)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def benchmark_size(size)
|
49
|
+
sep "#", "Benchmarks with N=#{size} (t=#{time}sec/benchmark)", big: true
|
50
|
+
io.puts
|
51
|
+
benchmark_push_n size
|
52
|
+
benchmark_push_n_then_pop_n size
|
53
|
+
benchmark_repeated_push_pop size
|
54
|
+
end
|
55
|
+
|
56
|
+
def benchmark_push_n(queue_size)
|
57
|
+
benchmarking("push N", "push_n", queue_size)
|
58
|
+
end
|
59
|
+
|
60
|
+
def benchmark_push_n_then_pop_n(queue_size)
|
61
|
+
benchmarking("push N then pop N", "push_n_pop_n", queue_size)
|
62
|
+
end
|
63
|
+
|
64
|
+
def benchmark_repeated_push_pop(queue_size)
|
65
|
+
benchmarking(
|
66
|
+
"Push/pop with pre-filled queue (size=N)", "push_pop", queue_size
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# TODO: move somewhere else...
|
73
|
+
def skip_profiling?(queue_size, impl)
|
74
|
+
impl.klass == DHeap::Benchmarks::PushAndResort && 10_000 < queue_size
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
78
|
+
|
79
|
+
def benchmarking(name, file, size)
|
80
|
+
Bundler.with_unbundled_env do
|
81
|
+
sep "==", "#{name} (N=#{size})"
|
82
|
+
cmd = %W[
|
83
|
+
bin/benchmark-driver
|
84
|
+
--bundler
|
85
|
+
--run-duration 6
|
86
|
+
--timeout 15
|
87
|
+
--runner ips_zero_fail
|
88
|
+
benchmarks/#{file}.yml
|
89
|
+
]
|
90
|
+
env = ENV.to_h.merge(
|
91
|
+
"BENCH_N" => size.to_s,
|
92
|
+
"RUBYLIB" => File.expand_path("../..", __dir__),
|
93
|
+
)
|
94
|
+
system(env, *cmd)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def sep(sep, msg = "", width: 80, big: false)
|
99
|
+
txt = String.new
|
100
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
101
|
+
txt += sep
|
102
|
+
txt += " #{msg}" if msg && !msg.empty?
|
103
|
+
txt += " " unless big
|
104
|
+
txt += sep * ((width - txt.length) / sep.length) unless big
|
105
|
+
txt += "\n"
|
106
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
107
|
+
io.print txt
|
108
|
+
end
|
109
|
+
|
110
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DHeap::Benchmarks
|
4
|
+
|
5
|
+
# base class for example priority queues
|
6
|
+
class ExamplePriorityQueue
|
7
|
+
attr_reader :a
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@a = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@a.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@a.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
if ENV["LOG_LEVEL"] == "debug"
|
22
|
+
def dbg(msg)
|
23
|
+
puts "%20s: %p, %p" % [msg, @a.first, (@a[1..-1] || []).each_slice(2).to_a]
|
24
|
+
end
|
25
|
+
else
|
26
|
+
def dbg(msg) nil end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# The most naive approach--completely unsorted!--is ironically not the worst.
|
32
|
+
class FindMin < ExamplePriorityQueue
|
33
|
+
|
34
|
+
# O(1)
|
35
|
+
def <<(score)
|
36
|
+
raise ArgumentError unless score
|
37
|
+
@a.push score
|
38
|
+
end
|
39
|
+
|
40
|
+
# O(n)
|
41
|
+
def pop
|
42
|
+
return unless (score = @a.min)
|
43
|
+
index = @a.rindex(score)
|
44
|
+
@a.delete_at(index)
|
45
|
+
score
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# Re-sorting after each insert: this both naive and performs the worst.
|
51
|
+
class Sorting < ExamplePriorityQueue
|
52
|
+
|
53
|
+
# O(n log n)
|
54
|
+
def <<(score)
|
55
|
+
raise ArgumentError unless score
|
56
|
+
@a.push score
|
57
|
+
@a.sort!
|
58
|
+
end
|
59
|
+
|
60
|
+
# O(1)
|
61
|
+
def pop
|
62
|
+
@a.shift
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
# A very simple example priority queue that is implemented with a sorted array.
|
68
|
+
#
|
69
|
+
# It uses Array#bsearch + Array#insert to push new values, and Array#pop to pop
|
70
|
+
# the min value.
|
71
|
+
class BSearch < ExamplePriorityQueue
|
72
|
+
|
73
|
+
# Array#bsearch_index is O(log n)
|
74
|
+
# Array#insert is O(n)
|
75
|
+
#
|
76
|
+
# So this should be O(n).
|
77
|
+
#
|
78
|
+
# In practice though, memcpy has a *very* small constant factor.
|
79
|
+
# And bsearch_index uses *exactly* (log n / log 2) comparisons.
|
80
|
+
def <<(score)
|
81
|
+
raise ArgumentError unless score
|
82
|
+
index = @a.bsearch_index {|other| score > other } || @a.length
|
83
|
+
@a.insert(index, score)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Array#pop is O(1). It updates length without changing capacity or contents.
|
87
|
+
#
|
88
|
+
# No comparisons are necessary.
|
89
|
+
#
|
90
|
+
# shift is usually also O(1) and could be used if it were sorted normally.
|
91
|
+
def pop
|
92
|
+
@a.pop
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# a very simple pure ruby binary heap
|
98
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
99
|
+
class RbHeap < ExamplePriorityQueue
|
100
|
+
|
101
|
+
def <<(score)
|
102
|
+
raise ArgumentError unless score
|
103
|
+
@a.push(score)
|
104
|
+
# shift up
|
105
|
+
index = @a.size - 1
|
106
|
+
while 0 < index # rubocop:disable Style/NumericPredicate
|
107
|
+
parent_index = (index - 1) / 2
|
108
|
+
break if @a[parent_index] <= @a[index]
|
109
|
+
@a[index] = @a[parent_index]
|
110
|
+
index = parent_index
|
111
|
+
@a[index] = score
|
112
|
+
# check_heap!(index)
|
113
|
+
end
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
def pop
|
118
|
+
return if @a.empty?
|
119
|
+
popped = @a.first
|
120
|
+
@a[0] = shifting = @a.last
|
121
|
+
@a.pop
|
122
|
+
# shift down
|
123
|
+
index = 0
|
124
|
+
last_index = @a.size - 1
|
125
|
+
while (child_index = index * 2 + 1) <= last_index
|
126
|
+
# select min child
|
127
|
+
if child_index < last_index && @a[child_index + 1] < @a[child_index]
|
128
|
+
child_index += 1
|
129
|
+
end
|
130
|
+
break if @a[index] <= @a[child_index]
|
131
|
+
@a[index] = @a[child_index]
|
132
|
+
index = child_index
|
133
|
+
@a[index] = shifting
|
134
|
+
end
|
135
|
+
popped
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def check_heap!(idx, last = @a.size - 1)
|
141
|
+
pscore = @a[idx]
|
142
|
+
child = idx * 2 + 1
|
143
|
+
if child <= last
|
144
|
+
cscore = check_heap!(child)
|
145
|
+
raise "#{pscore} > #{cscore}" if pscore > cscore
|
146
|
+
end
|
147
|
+
child += 1
|
148
|
+
if child <= last
|
149
|
+
check_heap!(child)
|
150
|
+
cscore = check_heap!(child)
|
151
|
+
raise "#{pscore} > #{cscore}" if pscore > cscore
|
152
|
+
end
|
153
|
+
pscore
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
158
|
+
|
159
|
+
# Different duck-typed priority queue implemenations
|
160
|
+
IMPLEMENTATIONS = [
|
161
|
+
OpenStruct.new(name: " push and resort", klass: Sorting).freeze,
|
162
|
+
OpenStruct.new(name: " find min + del", klass: FindMin).freeze,
|
163
|
+
OpenStruct.new(name: "bsearch + insert", klass: BSearch).freeze,
|
164
|
+
OpenStruct.new(name: "ruby binary heap", klass: RbHeap).freeze,
|
165
|
+
OpenStruct.new(name: "quaternary DHeap", klass: DHeap).freeze,
|
166
|
+
].freeze
|
167
|
+
|
168
|
+
end
|