d_heap 0.5.0 → 0.6.0

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.
@@ -8,10 +8,9 @@ require "mkmf"
8
8
  # $CFLAGS << " -g -ginline-points "
9
9
  # $CFLAGS << " -fno-omit-frame-pointer "
10
10
 
11
- # CONFIG["debugflags"] << " -ggdb3 -gstatement-frontiers -ginline-points "
12
- CONFIG["optflags"] << " -O3 "
13
- CONFIG["optflags"] << " -fno-omit-frame-pointer "
14
- CONFIG["warnflags"] << " -Werror"
11
+ if enable_config("debug")
12
+ CONFIG["warnflags"] << " -Werror -Wpedantic "
13
+ end
15
14
 
16
15
  have_func "rb_gc_mark_movable" # since ruby-2.7
17
16
 
Binary file
Binary file
Binary file
@@ -29,77 +29,115 @@ class BenchmarkDriver::Runner::IpsZeroFail < BenchmarkDriver::Runner::Ips
29
29
  class Job < BenchmarkDriver::DefaultJob
30
30
  attr_accessor :warmup_value, :warmup_duration, :warmup_loop_count
31
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
+
32
38
  end
33
39
 
34
40
  # BenchmarkDriver::Runner looks for this class
35
41
  JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
36
42
 
37
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
38
-
39
43
  # This method is dynamically called by `BenchmarkDriver::JobRunner.run`
40
44
  # @param [Array<BenchmarkDriver::Default::Job>] jobs
41
45
  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
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
+ }
67
57
  end
58
+ end
68
59
 
60
+ def run_all_jobs_benchmarks(jobs)
69
61
  @output.with_benchmark do
70
62
  jobs.each do |job|
71
63
  @output.with_job(name: job.name) do
72
64
  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
65
+ run_and_report_job(job, context)
96
66
  end
97
67
  end
98
68
  end
99
69
  end
100
70
  end
101
71
 
102
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
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
103
141
 
104
142
  def run_warmup(job, context:)
105
143
  start = Time.now
@@ -10,24 +10,80 @@ 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
17
- alias deq pop
18
- alias enq push
19
- alias first peek
20
- alias pop_below pop_lt
21
-
22
- alias length size
23
- alias count size
24
-
25
- # ruby 3.0+ (2.x can just use inherited initialize_clone)
26
- if Object.instance_method(:initialize_clone).arity == -1
27
- # @!visibility private
28
- def initialize_clone(other, freeze: nil)
29
- __init_clone__(other, freeze ? true : freeze)
30
- end
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)
31
87
  end
32
88
 
33
89
  # Consumes the heap by popping each minumum value until it is empty.
@@ -35,13 +91,20 @@ class DHeap
35
91
  # If you want to iterate over the heap without consuming it, you will need to
36
92
  # first call +#dup+
37
93
  #
94
+ # @param with_score [Boolean] if scores shoul also be yielded
95
+ #
38
96
  # @yieldparam value [Object] each value that would be popped
97
+ # @yieldparam score [Numeric] each value's score, if +with_scores+ is true
39
98
  #
40
99
  # @return [Enumerator] if no block is given
41
100
  # @return [nil] if a block is given
42
- def each_pop
43
- return to_enum(__method__) unless block_given?
44
- yield pop until empty?
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
45
108
  nil
46
109
  end
47
110
 
@@ -101,14 +101,28 @@ module DHeap::Benchmarks
101
101
  end
102
102
 
103
103
  # a very simple pure ruby binary heap
104
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
105
104
  class RbHeap < ExamplePriorityQueue
106
105
 
107
106
  def <<(value)
108
107
  raise ArgumentError unless value
109
108
  @a.push(value)
110
- # shift up
111
- index = @a.size - 1
109
+ sift_up(@a.size - 1, value)
110
+ end
111
+
112
+ def pop
113
+ return if @a.empty?
114
+ popped = @a.first
115
+ value = @a.pop
116
+ last_index = @a.size - 1
117
+ return popped unless 0 <= last_index
118
+
119
+ sift_down(0, last_index, value)
120
+ popped
121
+ end
122
+
123
+ private
124
+
125
+ def sift_up(index, value = @a[index])
112
126
  while 0 < index # rubocop:disable Style/NumericPredicate
113
127
  parent_index = (index - 1) / 2
114
128
  parent_value = @a[parent_index]
@@ -117,43 +131,28 @@ module DHeap::Benchmarks
117
131
  index = parent_index
118
132
  end
119
133
  @a[index] = value
120
- # dbg "__push__(%p)" % [value]
121
134
  # check_heap!(index)
122
135
  end
123
136
 
124
- def pop
125
- return if @a.empty?
126
- popped = @a.first
127
- value = @a.pop
128
- last_index = @a.size - 1
137
+ def sift_down(index, last_index = @a.size - 1, value = @a[index])
129
138
  last_parent = (last_index - 1) / 2
130
- return popped unless 0 <= last_index
131
-
132
- # sift down from 0
133
- index = 0
134
- child_index = 1
135
139
  while index <= last_parent
136
- child_value = @a[child_index]
137
- # select min child
138
- if child_index < last_index
139
- other_child_index = child_index + 1
140
- other_child_value = @a[other_child_index]
141
- if other_child_value < child_value
142
- child_value = other_child_value
143
- child_index = other_child_index
144
- end
145
- end
140
+ child_index, child_value = select_min_child(index, last_index)
146
141
  break if value <= child_value
147
142
  @a[index] = child_value
148
143
  index = child_index
149
144
  child_index = index * 2 + 1
150
145
  end
151
146
  @a[index] = value
152
-
153
- popped
154
147
  end
155
148
 
156
- private
149
+ def select_min_child(index, last_index = @a.size - 1)
150
+ child_index = index * 2 + 1
151
+ if child_index < last_index && a[child_index + 1] < @a[child_index]
152
+ child_index += 1
153
+ end
154
+ [child_index, @a[child_index]]
155
+ end
157
156
 
158
157
  def check_heap!(idx)
159
158
  check_heap_up!(idx)
@@ -186,7 +185,6 @@ module DHeap::Benchmarks
186
185
  end
187
186
 
188
187
  end
189
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
190
188
 
191
189
  # minor adjustments to the "priority_queue_cxx" gem, to match the API
192
190
  class CppSTL
@@ -201,6 +199,10 @@ module DHeap::Benchmarks
201
199
  @q = FastContainers::PriorityQueue.new(:min)
202
200
  end
203
201
 
202
+ def empty?
203
+ @q.empty?
204
+ end
205
+
204
206
  def pop
205
207
  @q.pop
206
208
  rescue RuntimeError
@@ -39,30 +39,6 @@ module DHeap::Benchmarks
39
39
  matcher :perform_at_least do |expected|
40
40
  supports_block_expectations
41
41
 
42
- def __debug__(name, caller_binding)
43
- lvars = __debug_lvars__(caller_binding)
44
- ivars = __debug_ivars__(caller_binding)
45
- puts "%s, locals => %p, ivars => %p" % [name, lvars, ivars]
46
- end
47
-
48
- def __debug_lvars__(caller_binding)
49
- caller_binding.local_variables.map {|lvar|
50
- next if %i[type unit].include?(lvar)
51
- next if (val = caller_binding.local_variable_get(lvar)).nil?
52
- [lvar, val]
53
- }.compact.to_h
54
- end
55
-
56
- def __debug_ivars__(caller_binding)
57
- instance_variables.map {|ivar|
58
- next if %i[@name @actual @expected_as_array @matcher_execution_context
59
- @chained_method_clauses @block_arg]
60
- .include?(ivar)
61
- next if (val = instance_variable_get(ivar)).nil?
62
- [ivar, val]
63
- }.compact.to_h
64
- end
65
-
66
42
  %i[
67
43
  is_at_least
68
44
  running_at_most
@@ -70,7 +46,6 @@ module DHeap::Benchmarks
70
46
  warmup_at_most
71
47
  ].each do |type|
72
48
  chain type do |number|
73
- # __debug__ "%s(%p)" % [type, number], binding
74
49
  reason, value = ___number_reason_and_value___
75
50
  if reason || value
76
51
  raise "Need to handle unit-less number first: %s(%p)" % [reason, value]
@@ -88,22 +63,15 @@ module DHeap::Benchmarks
88
63
  milliseconds
89
64
  ].each do |unit|
90
65
  chain unit do
91
- # __debug__ unit, binding
92
66
  reason, value = ___number_reason_and_value___
93
67
  raise "No number was specified" unless reason && value
94
- case reason
95
- when :running_at_most; apply_max_run unit
96
- when :running_at_least; apply_min_run unit
97
- when :warmup_at_most; apply_warmup unit
98
- else raise "%s is incompatible with %s(%p)" % [unit, reason, value]
99
- end
68
+ apply_number_to_reason(reason, value, unit)
100
69
  @number_for = @number_val = nil
101
70
  end
102
71
  end
103
72
 
104
73
  # TODO: let IPS set time to run instead of iterations to run
105
74
  chain :ips do
106
- # __debug__ "ips", binding
107
75
  reason, value = ___number_reason_and_value___
108
76
  raise "'ips' unit is only for assertions" unless reason == :is_at_least
109
77
  raise "Already asserting %s ips" % [@expect_ips] if @expect_ips
@@ -115,7 +83,6 @@ module DHeap::Benchmarks
115
83
 
116
84
  # need to use method because "chain" can't take a block
117
85
  def times_faster_than(&other)
118
- # __debug__ "times_faster_than"
119
86
  reason, value = ___number_reason_and_value___
120
87
  raise "'times_faster_than' is only for assertions" unless reason == :is_at_least
121
88
  raise "Already asserting %sx comparison" % [@expect_cmp] if @expect_cmp
@@ -174,7 +141,6 @@ module DHeap::Benchmarks
174
141
  chain :__convert_expected_to_ivars__ do
175
142
  @number_val ||= expected
176
143
  @number_for ||= :is_at_least if @number_val
177
- # __debug__ "__convert_expected_to_ivars__", binding
178
144
  expected = nil
179
145
  end
180
146
  private :__convert_expected_to_ivars__
@@ -184,30 +150,43 @@ module DHeap::Benchmarks
184
150
  [@number_for, @number_val]
185
151
  end
186
152
 
187
- def apply_min_run(unit)
153
+ def apply_number_to_reason(reason, value, unit)
154
+ normalized_value, normalized_unit = normalize_unit(unit)
155
+ case reason
156
+ when :running_at_most; apply_max_run normalized_value, normalized_unit
157
+ when :running_at_least; apply_min_run normalized_value, normalized_unit
158
+ when :warmup_at_most; apply_warmup normalized_value, normalized_unit
159
+ else raise "%s is incompatible with %s(%p)" % [unit, reason, value]
160
+ end
161
+ end
162
+
163
+ def normalize_unit(unit)
164
+ case unit
165
+ when :seconds; [Float(@number_val), :seconds]
166
+ when :milliseconds; [Float(@number_val) / 1000.0, :seconds]
167
+ when :times; [Integer(@number_val), :times]
168
+ else raise "Invalid unit %s for %s(%p)" % [unit, reason, value]
169
+ end
170
+ end
171
+
172
+ def apply_min_run(value, unit)
188
173
  case unit
189
- when :seconds; @min_time = Float(@number_val)
190
- when :milliseconds; @min_time = Float(@number_val) / 1000.0
191
- when :times; @min_iter = Integer(@number_val)
192
- else raise "Invalid unit %s for %s(%p)" % [unit, @number_for, @number_val]
174
+ when :seconds; @min_time = value
175
+ when :times; @min_iter = value
193
176
  end
194
177
  end
195
178
 
196
- def apply_max_run(unit)
179
+ def apply_max_run(value, unit)
197
180
  case unit
198
- when :seconds; @max_time = Float(@number_val)
199
- when :milliseconds; @max_time = Float(@number_val) / 1000.0
200
- when :times; @max_iter = Integer(@number_val)
201
- else raise "Invalid unit %s for %s(%p)" % [unit, @number_for, @number_val]
181
+ when :seconds; @max_time = value
182
+ when :times; @max_iter = value
202
183
  end
203
184
  end
204
185
 
205
- def apply_warmup(unit)
186
+ def apply_warmup(value, unit)
206
187
  case unit
207
- when :seconds; @warmup_time = Float(@number_val)
208
- when :milliseconds; @warmup_time = Float(@number_val) / 1000.0
209
- when :times; @warmup_iter = Integer(@number_val)
210
- else raise "Invalid unit %s for %s(%p)" % [unit, @number_for, @number_val]
188
+ when :seconds; @warmup_time = value
189
+ when :times; @warmup_iter = value
211
190
  end
212
191
  end
213
192
 
@@ -224,7 +203,6 @@ module DHeap::Benchmarks
224
203
 
225
204
  def run_measurements
226
205
  puts header if loud?
227
- # __debug__ "run_measurements", binding
228
206
  warmup
229
207
  take_measurements
230
208
  end