-
Notifications
You must be signed in to change notification settings - Fork 10
/
Rakefile
335 lines (269 loc) · 9.29 KB
/
Rakefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require 'benchmark/ips'
require 'qo'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
# Run a benchmark given a title and a set of benchmarks. Admittedly this
# is done because the Benchmark.ips code can get a tinge repetitive and this
# is easier to write out.
#
# @param title [String] Title of the benchmark
# @param **benchmarks [Hash[Symbol, Proc]] Name to Proc to run to benchmark it
#
# @note Notice I'm using `'String': -> {}` instead of hashrockets? Kwargs doesn't
# take string / hashrocket arguments, probably to prevent abuse of the
# "anything can be a key" bit of Ruby.
#
# @return [Unit] StdOut
def run_benchmark(title, quiet = false, **benchmarks)
puts '', title, '=' * title.size, ''
# Validation
benchmarks.each do |benchmark_name, benchmark_fn|
puts "#{benchmark_name} result: #{benchmark_fn.call()}"
end unless quiet
puts
Benchmark.ips do |bm|
benchmarks.each do |benchmark_name, benchmark_fn|
bm.report(benchmark_name, &benchmark_fn)
end
bm.compare!
end
end
def xrun_benchmark(title, **benchmarks) end
# Note that the current development of Qo is NOT to be performance first, it's to
# be readability first with performance coming later. That means that early iterations
# may well be slower, but the net expressiveness we get is worth it in the short run.
task :perf do
puts "Running on Qo v#{Qo::VERSION} at rev #{`git rev-parse HEAD`} - Ruby #{`ruby -v`}"
# Compare simple array equality. I almost think this isn't fair to Qo considering
# no sane dev should use it for literal 1 to 1 matches like this.
simple_array = [1, 1]
run_benchmark('Array * Array - Literal',
'Vanilla': -> {
simple_array == simple_array
},
'Qo.and': -> {
Qo.and(1, 1).call(simple_array)
}
)
# Compare testing indexed array matches. This gets a bit more into what Qo does,
# though I feel like there are optimizations that could be had here as well.
range_match_set = [1..10, 1..10, 1..10, 1..10]
range_match_target = [1, 2, 3, 4]
run_benchmark('Array * Array - Index pattern match',
'Vanilla': -> {
range_match_target.each_with_index.all? { |x, i| range_match_set[i] === x }
},
'Qo.and': -> {
Qo.and(1..10, 1..10, 1..10, 1..10).call(range_match_target)
}
)
# Now we're getting into things Qo makes sense for. Comparing an entire list
# against a stream of predicates to check
numbers_array = [1, 2.0, 3, 4]
run_benchmark('Array * Object - Predicate match',
'Vanilla': -> {
numbers_array.all? { |i| i.is_a?(Integer) && i.even? && (20..30).include?(i) }
},
'Qo.and': -> {
numbers_array.all?(&Qo.and(Integer, :even?, 20..30))
}
)
# This one is a bit interesting. The vanilla version is written to reflect that
# it has NO idea what the length of either set is, which is exactly what Qo
# has to deal with as well.
people_array_target = [
['Robert', 22],
['Roberta', 22],
['Foo', 42],
['Bar', 18]
]
people_array_query = [/Rob/, 15..25]
run_benchmark('Array * Array - Select index pattern match',
'Vanilla': -> {
people_array_target.select { |person|
person.each_with_index.all? { |a, i| people_array_query[i] === a }
}
},
'Qo.and': -> {
people_array_target.select(&Qo.and(/Rob/, 15..25))
}
)
people_hashes = people_array_target.map { |(name, age)| {name: name, age: age} }
run_benchmark('Hash * Hash - Hash intersection',
'Vanilla': -> {
people_hashes.select { |person| (15..25).include?(person[:age]) && /Rob/ =~ person[:name] }
},
'Qo.and': -> {
people_hashes.select(&Qo.and(name: /Rob/, age: 15..25))
}
)
Person = Struct.new(:name, :age)
people = [
Person.new('Robert', 22),
Person.new('Roberta', 22),
Person.new('Foo', 42),
Person.new('Bar', 17)
]
run_benchmark('Hash * Object - Property match',
'Vanilla': -> {
people.select { |person| (15..25).include?(person.age) && /Rob/ =~ person.name }
},
'Qo.and': -> {
people.select(&Qo.and(name: /Rob/, age: 15..25))
}
)
end
task :perf_pattern_match do
# Going to redefine the way that success and fail happen in here.
# return false
require 'dry-matcher'
# Match `[:ok, some_value]` for success
success_case = Dry::Matcher::Case.new(
match: -> value { value.first == :ok },
resolve: -> value { value.last }
)
# Match `[:err, some_error_code, some_value]` for failure
failure_case = Dry::Matcher::Case.new(
match: -> value, *pattern {
value[0] == :err && (pattern.any? ? pattern.include?(value[1]) : true)
},
resolve: -> value { value.last }
)
# Build the matcher
matcher = Dry::Matcher.new(success: success_case, failure: failure_case)
qo_m = Qo.result_match { |m|
m.success(Any) { |v| v }
m.failure(Any) { "ERR!" }
}
qo_m_case = proc { |target|
Qo.result_case(target) { |m|
m.success(Any) { |v| v }
m.failure(Any) { "ERR!" }
}
}
dm_m = proc { |target|
matcher.(target) do |m|
m.success { |(v)| v }
m.failure { 'ERR!' }
end
}
# v_m = proc { |target|
# next target[1] if target[0] == :ok
# 'ERR!'
# }
ok_target = [:ok, 12345]
err_target = [:err, "OH NO!"]
run_benchmark('Single Item Tuple',
'Qo': -> {
"OK: #{qo_m[ok_target]}, ERR: #{qo_m[err_target]}"
},
'Qo Case': -> {
"OK: #{qo_m_case[ok_target]}, ERR: #{qo_m_case[err_target]}"
},
'DryRB': -> {
"OK: #{dm_m[ok_target]}, ERR: #{dm_m[err_target]}"
},
# 'Vanilla': -> {
# "OK: #{v_m[ok_target]}, ERR: #{v_m[err_target]}"
# },
)
collection = [ok_target, err_target] * 2_000
run_benchmark('Large Tuple Collection', true,
'Qo': -> { collection.map(&qo_m) },
'Qo Case': -> { collection.map(&qo_m_case) },
'DryRB': -> { collection.map(&dm_m) },
# 'Vanilla': -> { collection.map(&v_m) }
)
# Person = Struct.new(:name, :age)
# people = [
# Person.new('Robert', 22),
# Person.new('Roberta', 22),
# Person.new('Foo', 42),
# Person.new('Bar', 17)
# ] * 1_000
# v_om = proc { |target|
# if /^F/.match?(target.name) && (30..50).include?(target.age)
# "It's foo!"
# else
# "Not foo"
# end
# }
# qo_om = Qo.match { |m|
# m.when(name: /^F/, age: 30..50) { "It's foo!" }
# m.else { "Not foo" }
# }
# run_benchmark('Large Object Collection', true,
# 'Qo': -> { people.map(&qo_om) },
# 'Vanilla': -> { people.map(&v_om) }
# )
end
# Below this mark are mostly my experiments to see what features perform a bit better
# than others, and are mostly left to check different versions of Ruby against eachother.
#
# Feel free to use them in development, but the general consensus of them is that
# `send` type methods are barely slower. One _could_ write an IIFE to get around
# that and maintain the flexibility but it's a net loss of clarity.
#
# Proc wise, they're all within margin of error. We just need to be really careful
# of the 2.4+ bug of lambdas not destructuring automatically, which will wreak
# havoc on hash matchers.
task :kwargs_vs_positional do
def add_kw(a:, b:, c:, d:) a + b + c + d end
def add_pos(a,b,c,d) a + b + c + d end
run_benchmark('Positional vs KW Args',
'keyword': -> { add_kw(a: 1, b: 2, c: 3, d: 4) },
'positional': -> { add_pos(1,2,3,4) }
)
end
task :perf_predicates do
array = (1..1000).to_a
run_benchmark('Predicates any?',
'block_any?': -> { array.any? { |v| v.even? } },
'proc_any?': -> { array.any?(&:even?) },
'send_proc_any?': -> { array.public_send(:any?, &:even?) }
)
run_benchmark('Predicates all?',
'block_all?': -> { array.all? { |v| v.even? } },
'proc_all?': -> { array.all?(&:even?) },
'send_proc_all?': -> { array.public_send(:all?, &:even?) }
)
run_benchmark('Predicates none?',
'block_none?': -> { array.none? { |v| v.even? } },
'proc_none?': -> { array.none?(&:even?) },
'send_proc_none?': -> { array.public_send(:none?, &:even?) },
)
even_stabby_lambda = -> n { n % 2 == 0 }
even_lambda = lambda { |n| n % 2 == 0 }
even_proc_new = Proc.new { |n| n % 2 == 0 }
even_proc_short = proc { |n| n % 2 == 0 }
even_to_proc = :even?.to_proc
run_benchmark('Types of Functions in Ruby',
even_stabby_lambda: -> { array.all?(&even_stabby_lambda) },
even_lambda: -> { array.all?(&even_lambda) },
even_proc_new: -> { array.all?(&even_proc_new) },
even_proc_short: -> { array.all?(&even_proc_short) },
even_to_proc: -> { array.all?(&even_to_proc) },
)
end
task :perf_random do
run_benchmark('Empty on blank array',
'empty?': -> { [].empty? },
'size == 0': -> { [].size == 0 },
'size.zero?': -> { [].size.zero? },
)
array = (1..1000).to_a
run_benchmark('Empty on several elements array',
'empty?': -> { array.empty? },
'size == 0': -> { array.size == 0 },
'size.zero?': -> { array.size.zero? },
)
hash = array.map { |v| [v, v] }.to_h
run_benchmark('Empty on blank hash vs array',
'hash empty?': -> { {}.empty? },
'array empty?': -> { [].empty? },
'full hash empty?': -> { hash.empty? },
'full array empty?': -> { array.empty? },
)
end