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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +26 -0
- data/.rubocop.yml +199 -0
- data/CHANGELOG.md +59 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +42 -5
- data/README.md +392 -109
- data/Rakefile +8 -2
- data/benchmarks/perf.rb +29 -0
- data/benchmarks/push_n.yml +31 -0
- data/benchmarks/push_n_pop_n.yml +35 -0
- data/benchmarks/push_pop.yml +27 -0
- data/benchmarks/stackprof.rb +31 -0
- data/bin/bench_n +7 -0
- data/bin/benchmark-driver +29 -0
- data/bin/benchmarks +10 -0
- data/bin/console +1 -0
- data/bin/profile +10 -0
- data/bin/rubocop +29 -0
- data/d_heap.gemspec +11 -6
- data/docs/benchmarks-2.txt +75 -0
- data/docs/benchmarks-mem.txt +39 -0
- data/docs/benchmarks.txt +515 -0
- data/docs/profile.txt +392 -0
- data/ext/d_heap/d_heap.c +555 -225
- data/ext/d_heap/d_heap.h +24 -48
- data/ext/d_heap/extconf.rb +20 -0
- data/lib/benchmark_driver/runner/ips_zero_fail.rb +120 -0
- data/lib/d_heap.rb +40 -2
- data/lib/d_heap/benchmarks.rb +112 -0
- data/lib/d_heap/benchmarks/benchmarker.rb +116 -0
- data/lib/d_heap/benchmarks/implementations.rb +222 -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 +4 -1
- metadata +54 -3
@@ -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
|