d_heap 0.2.0 → 0.5.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.
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fc"
4
+
5
+ module DHeap::Benchmarks
6
+
7
+ # base class for example priority queues
8
+ class ExamplePriorityQueue
9
+ attr_reader :a
10
+
11
+ # quick initialization by simply sorting the array once.
12
+ def initialize(count = nil, &block)
13
+ @a = []
14
+ return unless count
15
+ count.times {|i| @a << block.call(i) }
16
+ @a.sort!
17
+ end
18
+
19
+ def clear
20
+ @a.clear
21
+ end
22
+
23
+ def empty?
24
+ @a.empty?
25
+ end
26
+
27
+ if ENV["LOG_LEVEL"] == "debug"
28
+ def dbg(msg)
29
+ puts "%20s: %p, %p" % [msg, @a.first, (@a[1..-1] || []).each_slice(2).to_a]
30
+ end
31
+ else
32
+ def dbg(msg) nil end
33
+ end
34
+
35
+ end
36
+
37
+ # The most naive approach--completely unsorted!--is ironically not the worst.
38
+ class FindMin < ExamplePriorityQueue
39
+
40
+ # O(1)
41
+ def <<(score)
42
+ raise ArgumentError unless score
43
+ @a.push score
44
+ end
45
+
46
+ # O(n)
47
+ def pop
48
+ return unless (score = @a.min)
49
+ index = @a.rindex(score)
50
+ @a.delete_at(index)
51
+ score
52
+ end
53
+
54
+ end
55
+
56
+ # Re-sorting after each insert: this both naive and performs the worst.
57
+ class Sorting < ExamplePriorityQueue
58
+
59
+ # O(n log n)
60
+ def <<(score)
61
+ raise ArgumentError unless score
62
+ @a.push score
63
+ @a.sort!
64
+ end
65
+
66
+ # O(1)
67
+ def pop
68
+ @a.shift
69
+ end
70
+
71
+ end
72
+
73
+ # A very simple example priority queue that is implemented with a sorted array.
74
+ #
75
+ # It uses Array#bsearch + Array#insert to push new values, and Array#pop to pop
76
+ # the min value.
77
+ class BSearch < ExamplePriorityQueue
78
+
79
+ # Array#bsearch_index is O(log n)
80
+ # Array#insert is O(n)
81
+ #
82
+ # So this should be O(n).
83
+ #
84
+ # In practice though, memcpy has a *very* small constant factor.
85
+ # And bsearch_index uses *exactly* (log n / log 2) comparisons.
86
+ def <<(score)
87
+ raise ArgumentError unless score
88
+ index = @a.bsearch_index {|other| score > other } || @a.length
89
+ @a.insert(index, score)
90
+ end
91
+
92
+ # Array#pop is O(1). It updates length without changing capacity or contents.
93
+ #
94
+ # No comparisons are necessary.
95
+ #
96
+ # shift is usually also O(1) and could be used if it were sorted normally.
97
+ def pop
98
+ @a.pop
99
+ end
100
+
101
+ end
102
+
103
+ # a very simple pure ruby binary heap
104
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
105
+ class RbHeap < ExamplePriorityQueue
106
+
107
+ def <<(value)
108
+ raise ArgumentError unless value
109
+ @a.push(value)
110
+ # shift up
111
+ index = @a.size - 1
112
+ while 0 < index # rubocop:disable Style/NumericPredicate
113
+ parent_index = (index - 1) / 2
114
+ parent_value = @a[parent_index]
115
+ break if parent_value <= value
116
+ @a[index] = parent_value
117
+ index = parent_index
118
+ end
119
+ @a[index] = value
120
+ # dbg "__push__(%p)" % [value]
121
+ # check_heap!(index)
122
+ end
123
+
124
+ def pop
125
+ return if @a.empty?
126
+ popped = @a.first
127
+ value = @a.pop
128
+ last_index = @a.size - 1
129
+ 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
+ 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
146
+ break if value <= child_value
147
+ @a[index] = child_value
148
+ index = child_index
149
+ child_index = index * 2 + 1
150
+ end
151
+ @a[index] = value
152
+
153
+ popped
154
+ end
155
+
156
+ private
157
+
158
+ def check_heap!(idx)
159
+ check_heap_up!(idx)
160
+ check_heap_dn!(idx)
161
+ end
162
+
163
+ # compares index to its parent
164
+ def check_heap_at!(idx)
165
+ value = @a[idx]
166
+ unless idx <= 0
167
+ pidx = (idx - 1) / 2
168
+ pval = @a[pidx]
169
+ raise "@a[#{idx}] == #{value}, #{pval} > #{value}" if pval > value
170
+ end
171
+ value
172
+ end
173
+
174
+ def check_heap_up!(idx)
175
+ return if idx <= 0
176
+ pidx = (idx - 1) / 2
177
+ check_heap_at!(pidx)
178
+ check_heap_up!(pidx)
179
+ end
180
+
181
+ def check_heap_dn!(idx)
182
+ return unless @a.size <= idx
183
+ check_heap_at!(idx)
184
+ check_heap_down!(idx * 2 + 1)
185
+ check_heap_down!(idx * 2 + 2)
186
+ end
187
+
188
+ end
189
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
190
+
191
+ # minor adjustments to the "priority_queue_cxx" gem, to match the API
192
+ class CppSTL
193
+
194
+ def initialize
195
+ clear
196
+ end
197
+
198
+ def <<(value); @q.push(value, value) end
199
+
200
+ def clear
201
+ @q = FastContainers::PriorityQueue.new(:min)
202
+ end
203
+
204
+ def pop
205
+ @q.pop
206
+ rescue RuntimeError
207
+ nil
208
+ end
209
+
210
+ end
211
+
212
+ # Different duck-typed priority queue implemenations
213
+ IMPLEMENTATIONS = [
214
+ OpenStruct.new(name: " push and resort", klass: Sorting).freeze,
215
+ OpenStruct.new(name: " find min + del", klass: FindMin).freeze,
216
+ OpenStruct.new(name: "bsearch + insert", klass: BSearch).freeze,
217
+ OpenStruct.new(name: "ruby binary heap", klass: RbHeap).freeze,
218
+ OpenStruct.new(name: "C++STL PriorityQ", klass: CppSTL).freeze,
219
+ OpenStruct.new(name: "quaternary DHeap", klass: DHeap).freeze,
220
+ ].freeze
221
+
222
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "d_heap/benchmarks"
4
+
5
+ require "ruby-prof"
6
+
7
+ module DHeap::Benchmarks
8
+ # Profiles different implementations with different sizes
9
+ class Profiler
10
+ include Randomness
11
+ include Scenarios
12
+
13
+ N_COUNTS = [
14
+ 5, # 1 + 4
15
+ 1365, # 1 + 4 + 16 + 64 + 256 + 1024
16
+ 87_381, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384 + 65536
17
+ ].freeze
18
+
19
+ def call(
20
+ queue_size: ENV.fetch("PROFILE_QUEUE_SIZE", :unset),
21
+ iterations: ENV.fetch("PROFILE_ITERATIONS", 1_000_000)
22
+ )
23
+ DHeap::Benchmarks.puts_version_info("Profiling")
24
+ fill_random_vals
25
+ sizes = queue_size == :unset ? N_COUNTS : [Integer(queue_size)]
26
+ sizes.each do |size|
27
+ profile_all(size, iterations)
28
+ end
29
+ end
30
+
31
+ def profile_all(queue_size, iterations, io: $stdout)
32
+ io.puts <<~TEXT
33
+ ########################################################################
34
+ # Profile w/ N=#{queue_size} (i=#{iterations})
35
+ # (n.b. RubyProf & tracepoint can change relative performance.
36
+ # A sampling profiler can provide more accurate relative metrics.
37
+ ########################################################################
38
+
39
+ TEXT
40
+ DHeap::Benchmarks::IMPLEMENTATIONS.each do |impl|
41
+ profile_one(impl, queue_size, iterations, io: io)
42
+ end
43
+ end
44
+
45
+ # TODO: move somewhere else...
46
+ def skip_profiling?(queue_size, impl)
47
+ impl.klass == DHeap::Benchmarks::Sorting && 10_000 < queue_size
48
+ end
49
+
50
+ def profile_one(impl, queue_size, iterations, io: $stdout)
51
+ return if skip_profiling?(queue_size, impl)
52
+ io.puts "Filling #{impl.name} ---------------------------"
53
+ queue = impl.klass.new
54
+ push_n(queue, queue_size)
55
+ io.puts "Profiling #{impl.name} ---------------------------"
56
+ profiling do
57
+ repeated_push_pop(queue, iterations)
58
+ end
59
+ end
60
+
61
+ def profiling(io: $stdout, &block)
62
+ # do the thing
63
+ result = RubyProf.profile(&block)
64
+ # report_the_thing
65
+ printer = RubyProf::FlatPrinter.new(result)
66
+ printer.print($stdout, min_percent: 1.0)
67
+ io.puts
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "d_heap/benchmarks"
4
+
5
+ module DHeap::Benchmarks
6
+
7
+ # Profiles different implementations with different sizes
8
+ module RSpecMatchers # rubocop:disable Metrics/ModuleLength
9
+ extend RSpec::Matchers::DSL
10
+
11
+ # Assert ips (iterations per second):
12
+ #
13
+ # expect { ... }.to perform_at_least(1_000_000).ips
14
+ # .running_at_least(10).times # optional, defaults to 1
15
+ # .running_at_least(10).seconds # optional, defaults to 1s
16
+ # .running_at_most(10_000_000).times # optional, defaults to nil
17
+ # .running_at_most(2).seconds # optional, defaults to 2s
18
+ # .warmup_at_most(1000).times # optional, defaults to 1k
19
+ # .warmup_at_most(0.100).seconds # optional, defaults to 0.1s
20
+ # .iterations_per_round # optional, defaults to 1
21
+ # .and_at_least(1.1).times.faster_than { ... } # can also compare
22
+ #
23
+ # Assert comparison (and optionally runtime or ips):
24
+ #
25
+ # expect { ... }.to perform_at_least(2.5).times_faster_than { ... }
26
+ # .running_at_least(10).times # optional, defaults to 1
27
+ # .running_at_least(10).seconds # optional, defaults to 1s
28
+ # .running_at_most(10_000_000).times # optional, defaults to nil
29
+ # .running_at_most(2).seconds # optional, defaults to 2s
30
+ # .warmup_at_most(1000).times # optional, defaults to 1k
31
+ # .warmup_at_most(0.100).seconds # optional, defaults to 0.1s
32
+ # .iterations_per_call # optional, defaults to 1
33
+ # .and_at_least(100).ips { ... } # can also assert ips
34
+ #
35
+ # n.b: Given a known constant number of iterations, run time and ips are both
36
+ # measuring the same underlying metric.
37
+ #
38
+ # rubocop:disable Metrics/BlockLength, Layout/SpaceAroundOperators
39
+ matcher :perform_at_least do |expected|
40
+ supports_block_expectations
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
+ %i[
67
+ is_at_least
68
+ running_at_most
69
+ running_at_least
70
+ warmup_at_most
71
+ ].each do |type|
72
+ chain type do |number|
73
+ # __debug__ "%s(%p)" % [type, number], binding
74
+ reason, value = ___number_reason_and_value___
75
+ if reason || value
76
+ raise "Need to handle unit-less number first: %s(%p)" % [reason, value]
77
+ end
78
+ @number_for = type
79
+ @number_val = number
80
+ end
81
+ end
82
+
83
+ alias_method :and_at_least, :is_at_least
84
+
85
+ %i[
86
+ times
87
+ seconds
88
+ milliseconds
89
+ ].each do |unit|
90
+ chain unit do
91
+ # __debug__ unit, binding
92
+ reason, value = ___number_reason_and_value___
93
+ 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
100
+ @number_for = @number_val = nil
101
+ end
102
+ end
103
+
104
+ # TODO: let IPS set time to run instead of iterations to run
105
+ chain :ips do
106
+ # __debug__ "ips", binding
107
+ reason, value = ___number_reason_and_value___
108
+ raise "'ips' unit is only for assertions" unless reason == :is_at_least
109
+ raise "Already asserting %s ips" % [@expect_ips] if @expect_ips
110
+ raise "'ips' assertion has already been made" if @expect_ips
111
+ raise "Unknown assertion count" unless value
112
+ @expect_ips = Integer(value)
113
+ @number_for = @number_val = nil
114
+ end
115
+
116
+ # need to use method because "chain" can't take a block
117
+ def times_faster_than(&other)
118
+ # __debug__ "times_faster_than"
119
+ reason, value = ___number_reason_and_value___
120
+ raise "'times_faster_than' is only for assertions" unless reason == :is_at_least
121
+ raise "Already asserting %sx comparison" % [@expect_cmp] if @expect_cmp
122
+ raise ArgumentError, "must provide a proc" unless other
123
+ @expect_cmp = Float(value)
124
+ @cmp_proc = other
125
+ @number_for = @number_val = nil
126
+ self
127
+ end
128
+
129
+ chain :loudly do @volume = :loud end
130
+ chain :quietly do @volume = :quiet end
131
+ chain :volume do |volume|
132
+ raise "Invalid volume" unless %i[loud quiet].include?(volume)
133
+ @volume = volume
134
+ end
135
+
136
+ chain :iterations_per_round do |iterations|
137
+ if @iterations_per_round
138
+ raise "Already set iterations per round (%p)" [@iterations_per_round]
139
+ end
140
+ @iterations_per_round = Integer(iterations)
141
+ end
142
+
143
+ match do |actual|
144
+ require "benchmark"
145
+ raise "Need to expect a proc or block" unless actual.respond_to?(:to_proc)
146
+ raise "Need a performance assertion" unless assertion?
147
+ @actual_proc = actual
148
+ prepare_for_measurement
149
+ if @max_iter && (@max_iter % @iterations_per_round) != 0
150
+ raise "Iterations per round (%p) must divide evenly by max iterations (%p)" % [
151
+ @iterations_per_round, @max_iter,
152
+ ]
153
+ end
154
+ run_measurements
155
+ cmp_okay? && ips_okay?
156
+ end
157
+
158
+ description do
159
+ [
160
+ @expect_cmp && cmp_okay_msg,
161
+ @expect_ips && ips_okay_msg,
162
+ ].join(", and ")
163
+ end
164
+
165
+ failure_message do
166
+ [
167
+ cmp_okay? ? nil : "expected to #{cmp_okay_msg} but #{cmp_fail_msg}", # =>
168
+ ips_okay? ? nil : "expected to #{ips_okay_msg} but #{ips_fail_msg}",
169
+ ].compact.join(", and ")
170
+ end
171
+
172
+ private
173
+
174
+ chain :__convert_expected_to_ivars__ do
175
+ @number_val ||= expected
176
+ @number_for ||= :is_at_least if @number_val
177
+ # __debug__ "__convert_expected_to_ivars__", binding
178
+ expected = nil
179
+ end
180
+ private :__convert_expected_to_ivars__
181
+
182
+ def ___number_reason_and_value___
183
+ __convert_expected_to_ivars__
184
+ [@number_for, @number_val]
185
+ end
186
+
187
+ def apply_min_run(unit)
188
+ 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]
193
+ end
194
+ end
195
+
196
+ def apply_max_run(unit)
197
+ 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]
202
+ end
203
+ end
204
+
205
+ def apply_warmup(unit)
206
+ 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]
211
+ end
212
+ end
213
+
214
+ def prepare_for_measurement
215
+ @volume ||= ENV.fetch("RSPEC_BENCHMARK_VOLUME", :quiet).to_sym
216
+ @max_time ||= 2
217
+ @min_time ||= 1
218
+ @min_iter ||= 1
219
+ @warmup_time ||= 0.100
220
+ @warmup_iter ||= 1000
221
+ @iterations_per_round ||= 1
222
+ nil
223
+ end
224
+
225
+ def run_measurements
226
+ puts header if loud?
227
+ # __debug__ "run_measurements", binding
228
+ warmup
229
+ take_measurements
230
+ end
231
+
232
+ def header
233
+ max_rounds = @max_iter && @max_iter / @iterations_per_round
234
+ [
235
+ "Warmup time %s, or iterations: %s" % [@min_iter, @max_iter],
236
+ "Benchmark time (%s..%s) or iterations (%s..%s), max rounds: %p" % [
237
+ @min_time, @max_time, @min_iter, @max_iter, max_rounds,
238
+ ],
239
+ "%-10s %s" % ["", Benchmark::CAPTION],
240
+ ].join("\n")
241
+ end
242
+
243
+ def warmup
244
+ return unless 0 < @warmup_time && 0 < @warmup_iter # rubocop:disable Style/NumericPredicate
245
+ args = [@warmup_iter, 0, @warmup_time, 1, @warmup_iter]
246
+ measure("warmup", *args, &@actual_proc)
247
+ measure("warmup cmp", *args, &@cmp_proc) if @cmp_proc
248
+ end
249
+
250
+ def take_measurements
251
+ args = [@iterations_per_round, @min_time, @max_time, @min_iter, @max_iter]
252
+ @actual_tms = measure("actual", *args, &@actual_proc)
253
+ @cmp_tms = measure("cmp", *args, &@cmp_proc) if @cmp_proc
254
+ return unless @cmp_proc
255
+ # how many times faster?
256
+ @actual_cmp = @actual_tms.ips_real / @cmp_tms.ips_real
257
+ puts "Ran %0.3fx as fast as comparison" % [@actual_cmp] if loud?
258
+ end
259
+
260
+ def loud?; @volume == :loud end
261
+
262
+ def assertion?; !!(@expect_cmp || @expect_ips) end
263
+
264
+ def cmp_okay?; !@expect_cmp || @expect_cmp < @actual_cmp end
265
+ def ips_okay?; !@expect_tms || @expect_tms.ips < @actual_tms.ips end
266
+
267
+ def measure(name, ipr, *args)
268
+ measurements = TmsMeasurements.new(name, ipr, *args)
269
+ measurements.max_rounds.times do
270
+ # GC.start(full_mark: true, immediate_sweep: true)
271
+ # GC.compact
272
+ measurements << Benchmark.measure do
273
+ yield ipr
274
+ end
275
+ # p measurements.real
276
+ break if measurements.max_time < measurements.real
277
+ end
278
+ log_measurement(name, measurements)
279
+ measurements
280
+ end
281
+
282
+ # rubocop:disable Metrics/AbcSize
283
+ def units_str(num)
284
+ if num >= 10**12; "%7.3fT" % [num.to_f / 10**12]
285
+ elsif num >= 10** 9; "%7.3fB" % [num.to_f / 10** 9]
286
+ elsif num >= 10** 6; "%7.3fM" % [num.to_f / 10** 6]
287
+ elsif num >= 10** 3; "%7.3fk" % [num.to_f / 10** 3]
288
+ else "%7.3f" % [num.to_f]
289
+ end
290
+ end
291
+ # rubocop:enable Metrics/AbcSize
292
+
293
+ def log_measurement(name, measurements)
294
+ return unless loud?
295
+ puts "%-10s %s => %s ips (%d rounds)" % [
296
+ name,
297
+ measurements.tms.to_s.rstrip,
298
+ units_str(measurements.ips_real),
299
+ measurements.size,
300
+ ]
301
+ end
302
+
303
+ def cmp_okay_msg; "run %0.2fx faster" % [@expect_cmp] end
304
+ def cmp_fail_msg; "was only %0.2fx as fast" % [@actual_cmp] end
305
+ def ips_okay_msg; "run with %s ips" % [units_str(@expect_ips)] end
306
+ def ips_fail_msg; "was only %s ips" % [units_str(@actual_ips)] end
307
+
308
+ end
309
+ # rubocop:enable Metrics/BlockLength, Layout/SpaceAroundOperators
310
+
311
+ alias_matcher :perform_with, :perform
312
+
313
+ end
314
+
315
+ # Replicates a subset of the functionality in benchmark-ips
316
+ #
317
+ # TODO: merge this with benchmark-ips
318
+ # TODO: implement (or remove) min_time, min_iter
319
+ class TmsMeasurements
320
+ attr_reader :iterations_per_entry
321
+ attr_reader :iterations
322
+
323
+ attr_reader :min_time
324
+ attr_reader :max_time
325
+
326
+ attr_reader :min_iter
327
+ attr_reader :max_iter
328
+
329
+ def initialize(name, ipe, min_time, max_time, min_iter, max_iter) # rubocop:disable Metrics/ParameterLists
330
+ @name = name
331
+ @iterations_per_entry = Integer(ipe)
332
+ @min_time = Float(min_time)
333
+ @max_time = Float(max_time)
334
+ @min_iter = Integer(min_iter)
335
+ @max_iter = Integer(max_iter)
336
+ @entries = []
337
+ @sum = Benchmark::Tms.new
338
+ @iterations = 0
339
+ end
340
+
341
+ def size; entries.size end
342
+
343
+ def <<(tms)
344
+ raise TypeError, "not a #{Benchmark::Tms}" unless tms.is_a?(Benchmark::Tms)
345
+ raise IndexError, "full" if @max_iter <= size
346
+ @sum += tms
347
+ @iterations += @iterations_per_entry
348
+ @entries << tms
349
+ self
350
+ end
351
+
352
+ def sum; @sum.dup end
353
+ alias tms sum
354
+
355
+ def entries; @entries.dup end
356
+
357
+ def cstime; @sum.cstime end
358
+ def cutime; @sum.cutime end
359
+ def real; @sum.real end
360
+ def stime; @sum.stime end
361
+ def total; @sum.total end
362
+ def utime; @sum.utime end
363
+
364
+ def ips_real; @iterations / real end
365
+ def ips_total; @iterations / total end
366
+ def ips_utime; @iterations / utime end
367
+
368
+ def max_rounds
369
+ @max_iter && @max_iter / @iterations_per_entry
370
+ end
371
+
372
+ end
373
+
374
+ end