d_heap 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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