d_heap 0.2.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,21 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
- # if /darwin/ =~ RUBY_PLATFORM
6
- # $CFLAGS << " -D__D_HEAP_DEBUG"
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
7
+ # $CFLAGS << " -debug inline-debug-info "
8
+ # $CFLAGS << " -g -ginline-points "
9
+ # $CFLAGS << " -fno-omit-frame-pointer "
10
+
11
+ if enable_config("debug")
12
+ CONFIG["warnflags"] << " -Werror -Wpedantic "
13
+ end
14
+
15
+ have_func "rb_gc_mark_movable" # since ruby-2.7
16
+
17
+ check_sizeof("long")
18
+ check_sizeof("unsigned long long")
19
+ check_sizeof("long double")
20
+ have_macro("LDBL_MANT_DIG", "float.h")
8
21
 
9
22
  create_makefile("d_heap/d_heap")
Binary file
Binary file
Binary file
@@ -0,0 +1,158 @@
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
+ def add_warmup_attrs(value, duration, loop_count)
33
+ self.warmup_value = value
34
+ self.warmup_duration = duration
35
+ self.warmup_loop_count = loop_count
36
+ end
37
+
38
+ end
39
+
40
+ # BenchmarkDriver::Runner looks for this class
41
+ JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
42
+
43
+ # This method is dynamically called by `BenchmarkDriver::JobRunner.run`
44
+ # @param [Array<BenchmarkDriver::Default::Job>] jobs
45
+ def run(jobs)
46
+ jobs = run_all_jobs_warmup(jobs)
47
+ run_all_jobs_benchmarks(jobs)
48
+ end
49
+
50
+ def run_all_jobs_warmup(jobs)
51
+ return jobs if jobs.all?(&:loop_count)
52
+ @output.with_warmup do
53
+ jobs.map! {|job|
54
+ # skip warmup if loop_count is set
55
+ job.loop_count ? job : output_warmup_and_config_job(job)
56
+ }
57
+ end
58
+ end
59
+
60
+ def run_all_jobs_benchmarks(jobs)
61
+ @output.with_benchmark do
62
+ jobs.each do |job|
63
+ @output.with_job(name: job.name) do
64
+ job.runnable_contexts(@contexts).each do |context|
65
+ run_and_report_job(job, context)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def output_warmup_and_config_job(job)
73
+ @output.with_job(name: job.name) do
74
+ context = job.runnable_contexts(@contexts).first
75
+ value, duration, warmup_loop_count = run_and_report_warmup_job(job, context)
76
+ loop_count = (warmup_loop_count.to_f * @config.run_duration / duration).floor
77
+ Job.new(**job.to_h.merge(loop_count: loop_count))
78
+ .tap {|j| j.add_warmup_attrs(value, duration, warmup_loop_count) }
79
+ end
80
+ end
81
+
82
+ def run_and_report_warmup_job(job, context)
83
+ duration, loop_count = run_warmup(job, context: context)
84
+ value, duration = value_duration(duration: duration, loop_count: loop_count)
85
+ output_with_context(context) do
86
+ @output.report(
87
+ values: {metric => value}, duration: duration, loop_count: loop_count
88
+ )
89
+ end
90
+ [value, duration, loop_count]
91
+ end
92
+
93
+ def run_and_report_job(job, context)
94
+ result, loop_count = run_job_with_repeater(job, context)
95
+ value, duration = result.value
96
+ output_with_context(context) do
97
+ @output.report(
98
+ values: { metric => value },
99
+ all_values: { metric => result.all_values },
100
+ duration: duration,
101
+ loop_count: loop_count,
102
+ )
103
+ end
104
+ end
105
+
106
+ def output_with_context(context, &block)
107
+ @output.with_context(
108
+ name: context.name,
109
+ executable: context.executable,
110
+ gems: context.gems,
111
+ prelude: context.prelude,
112
+ &block
113
+ )
114
+ end
115
+
116
+ def run_job_with_repeater(job, context)
117
+ repeat_params = { config: @config, larger_better: true, rest_on_average: :average }
118
+ if job.loop_count&.positive?
119
+ run_job_with_own_loop_count(job, context, repeat_params)
120
+ else
121
+ run_job_with_warmup_loop_count(job, context, repeat_params)
122
+ end
123
+ end
124
+
125
+ def run_job_with_own_loop_count(job, context, repeat_params)
126
+ loop_count = job.loop_count
127
+ result = BenchmarkDriver::Repeater.with_repeat(**repeat_params) {
128
+ run_benchmark(job, context: context)
129
+ }
130
+ [result, loop_count]
131
+ end
132
+
133
+ def run_job_with_warmup_loop_count(job, context, repeat_params)
134
+ loop_count = job.warmup_loop_count
135
+ repeater_value = [job.warmup_value, job.warmup_duration]
136
+ result = BenchmarkDriver::Repeater::RepeatResult.new(
137
+ value: repeater_value, all_values: [repeater_value]
138
+ )
139
+ [result, loop_count]
140
+ end
141
+
142
+ def run_warmup(job, context:)
143
+ start = Time.now
144
+ super(job, context: context)
145
+ rescue Timeout::Error
146
+ [Time.now - start, 0.0.next_float]
147
+ end
148
+
149
+ def execute(*args, exception: true)
150
+ super
151
+ rescue RuntimeError => ex
152
+ if args.include?("timeout") && $CHILD_STATUS&.exitstatus == 124
153
+ raise Timeout::Error, ex.message
154
+ end
155
+ raise ex
156
+ end
157
+
158
+ end
@@ -10,13 +10,102 @@ require "d_heap/version"
10
10
  # the nodes have _d_ children instead of 2. This allows for "decrease priority"
11
11
  # operations to be performed more quickly with the tradeoff of slower delete
12
12
  # minimum. Additionally, _d_-ary heaps can have better memory cache behavior than
13
- # binary heaps, allowing them to run more quickly in practice despite slower
13
+ # binary heaps, allowing them to pop more quickly in practice despite slower
14
14
  # worst-case time complexity.
15
15
  #
16
+ # Although _d_ can be configured when creating the heap, it's usually best to
17
+ # keep the default value of 4, because d=4 gives the smallest coefficient for
18
+ # <tt>(d + 1) log n / log d</tt> result. As always, use benchmarks for your
19
+ # particular use-case.
20
+ #
21
+ # @example Basic push, peek, and pop
22
+ # # create some example objects to place in our heap
23
+ # Task = Struct.new(:id, :time) do
24
+ # def to_f; time.to_f end
25
+ # end
26
+ # t1 = Task.new(1, Time.now + 5*60)
27
+ # t2 = Task.new(2, Time.now + 50)
28
+ # t3 = Task.new(3, Time.now + 60)
29
+ # t4 = Task.new(4, Time.now + 5)
30
+ #
31
+ # # create the heap
32
+ # require "d_heap"
33
+ # heap = DHeap.new
34
+ #
35
+ # # push with an explicit score (which might be extrinsic to the value)
36
+ # heap.push t1, t1.to_f
37
+ #
38
+ # # the score will be implicitly cast with Float, so any object with #to_f
39
+ # heap.push t2, t2
40
+ #
41
+ # # if the object has an intrinsic score via #to_f, "<<" is the simplest API
42
+ # heap << t3 << t4
43
+ #
44
+ # # pop returns the lowest scored item, and removes it from the heap
45
+ # heap.pop # => #<struct Task id=4, time=2021-01-17 17:02:22.5574 -0500>
46
+ # heap.pop # => #<struct Task id=2, time=2021-01-17 17:03:07.5574 -0500>
47
+ #
48
+ # # peek returns the lowest scored item, without removing it from the heap
49
+ # heap.peek # => #<struct Task id=3, time=2021-01-17 17:03:17.5574 -0500>
50
+ # heap.pop # => #<struct Task id=3, time=2021-01-17 17:03:17.5574 -0500>
51
+ #
52
+ # # pop_lte handles the common "h.pop if h.peek_score < max" pattern
53
+ # heap.pop_lte(Time.now + 65) # => nil
54
+ #
55
+ # # the heap size can be inspected with size and empty?
56
+ # heap.empty? # => false
57
+ # heap.size # => 1
58
+ # heap.pop # => #<struct Task id=1, time=2021-01-17 17:07:17.5574 -0500>
59
+ # heap.empty? # => true
60
+ # heap.size # => 0
61
+ #
62
+ # # popping from an empty heap returns nil
63
+ # heap.pop # => nil
64
+ #
16
65
  class DHeap
66
+ alias deq pop
67
+ alias shift pop
68
+ alias next pop
69
+ alias pop_all_lt pop_all_below
70
+ alias pop_below pop_lt
71
+
72
+ alias enq push
73
+
74
+ alias first peek
75
+
76
+ alias length size
77
+ alias count size
78
+
79
+ # Initialize a _d_-ary min-heap.
80
+ #
81
+ # @param d [Integer] Number of children for each parent node.
82
+ # Higher values generally speed up push but slow down pop.
83
+ # If all pushes are popped, the default is probably best.
84
+ # @param capacity [Integer] initial capacity of the heap.
85
+ def initialize(d: DEFAULT_D, capacity: DEFAULT_CAPA) # rubocop:disable Naming/MethodParameterName
86
+ __init_without_kw__(d, capacity)
87
+ end
17
88
 
18
- def initialize_copy(other)
19
- raise NotImplementedError, "initialize_copy should deep copy array"
89
+ # Consumes the heap by popping each minumum value until it is empty.
90
+ #
91
+ # If you want to iterate over the heap without consuming it, you will need to
92
+ # first call +#dup+
93
+ #
94
+ # @param with_score [Boolean] if scores shoul also be yielded
95
+ #
96
+ # @yieldparam value [Object] each value that would be popped
97
+ # @yieldparam score [Numeric] each value's score, if +with_scores+ is true
98
+ #
99
+ # @return [Enumerator] if no block is given
100
+ # @return [nil] if a block is given
101
+ def each_pop(with_scores: false)
102
+ return to_enum(__method__, with_scores: with_scores) unless block_given?
103
+ if with_scores
104
+ yield(*pop_with_score) until empty?
105
+ else
106
+ yield pop until empty?
107
+ end
108
+ nil
20
109
  end
21
110
 
22
111
  end
@@ -0,0 +1,112 @@
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, clear: false)
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.clear if clear
105
+ queue
106
+ end
107
+
108
+ # rubocop:enable Style/NumericPredicate
109
+
110
+ require "d_heap/benchmarks/implementations"
111
+
112
+ end
@@ -0,0 +1,116 @@
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
+ if file == "push_n"
91
+ cmd << "--filter" << /dheap|\bstl\b|\bbsearch\b|\brb_heap\b/.to_s
92
+ end
93
+ env = ENV.to_h.merge(
94
+ "BENCH_N" => size.to_s,
95
+ "RUBYLIB" => File.expand_path("../..", __dir__),
96
+ )
97
+ system(env, *cmd)
98
+ end
99
+ end
100
+
101
+ def sep(sep, msg = "", width: 80, big: false)
102
+ txt = String.new
103
+ txt += "#{sep * (width / sep.length)}\n" if big
104
+ txt += sep
105
+ txt += " #{msg}" if msg && !msg.empty?
106
+ txt += " " unless big
107
+ txt += sep * ((width - txt.length) / sep.length) unless big
108
+ txt += "\n"
109
+ txt += "#{sep * (width / sep.length)}\n" if big
110
+ io.print txt
111
+ end
112
+
113
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
114
+
115
+ end
116
+ end