memo_wise 1.1.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +2 -2
- data/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +13 -1
- data/CHANGELOG.md +46 -6
- data/Gemfile.lock +29 -29
- data/README.md +72 -39
- data/benchmarks/Gemfile +2 -2
- data/benchmarks/benchmarks.rb +103 -179
- data/lib/memo_wise/internal_api.rb +110 -202
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +95 -137
- data/memo_wise.gemspec +1 -0
- metadata +4 -3
data/benchmarks/benchmarks.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "benchmark/ips"
|
4
4
|
|
5
|
+
require "tempfile"
|
5
6
|
require "memo_wise"
|
6
7
|
|
7
8
|
# Some gems do not yet work in Ruby 3 so we only require them if they're loaded
|
@@ -49,7 +50,6 @@ end
|
|
49
50
|
# using it to minimize the chance that our benchmarks are affected by ordering.
|
50
51
|
# NOTE: Some gems do not yet work in Ruby 3 so we only test with them if they've
|
51
52
|
# been `require`d.
|
52
|
-
# rubocop:disable Layout/LineLength
|
53
53
|
BENCHMARK_GEMS = [
|
54
54
|
BenchmarkGem.new(MemoWise, "prepend MemoWise", :memo_wise),
|
55
55
|
(BenchmarkGem.new(DDMemoize, "DDMemoize.activate(self)", :memoize) if defined?(DDMemoize)),
|
@@ -59,17 +59,11 @@ BENCHMARK_GEMS = [
|
|
59
59
|
(BenchmarkGem.new(Memoized, "include Memoized", :memoize) if defined?(Memoized)),
|
60
60
|
(BenchmarkGem.new(Memoizer, "include Memoizer", :memoize) if defined?(Memoizer))
|
61
61
|
].compact.shuffle
|
62
|
-
# rubocop:enable Layout/LineLength
|
63
62
|
|
64
63
|
# Use metaprogramming to ensure that each class is created in exactly the
|
65
64
|
# the same way.
|
66
65
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
67
|
-
# rubocop:disable Security/Eval
|
68
|
-
eval <<-CLASS, binding, __FILE__, __LINE__ + 1
|
69
|
-
# For these methods, we alternately return truthy and falsey values in
|
70
|
-
# order to benchmark memoization when the result of a method is falsey.
|
71
|
-
#
|
72
|
-
# We do this by checking if the first argument to a method is even.
|
66
|
+
eval <<~HEREDOC, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
|
73
67
|
class #{benchmark_gem.klass}Example
|
74
68
|
#{benchmark_gem.activation_code}
|
75
69
|
|
@@ -78,243 +72,173 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
78
72
|
end
|
79
73
|
#{benchmark_gem.memoization_method} :no_args
|
80
74
|
|
81
|
-
# For the no_args case, we can't depend on arguments to alternate between
|
82
|
-
# returning truthy and falsey values, so instead make two separate
|
83
|
-
# no_args methods
|
84
|
-
def no_args_falsey
|
85
|
-
nil
|
86
|
-
end
|
87
|
-
#{benchmark_gem.memoization_method} :no_args_falsey
|
88
|
-
|
89
75
|
def one_positional_arg(a)
|
90
|
-
100
|
76
|
+
100
|
91
77
|
end
|
92
78
|
#{benchmark_gem.memoization_method} :one_positional_arg
|
93
79
|
|
94
80
|
def positional_args(a, b)
|
95
|
-
100
|
81
|
+
100
|
96
82
|
end
|
97
83
|
#{benchmark_gem.memoization_method} :positional_args
|
98
84
|
|
99
85
|
def one_keyword_arg(a:)
|
100
|
-
100
|
86
|
+
100
|
101
87
|
end
|
102
88
|
#{benchmark_gem.memoization_method} :one_keyword_arg
|
103
89
|
|
104
90
|
def keyword_args(a:, b:)
|
105
|
-
100
|
91
|
+
100
|
106
92
|
end
|
107
93
|
#{benchmark_gem.memoization_method} :keyword_args
|
108
94
|
|
109
95
|
def positional_and_keyword_args(a, b:)
|
110
|
-
100
|
96
|
+
100
|
111
97
|
end
|
112
98
|
#{benchmark_gem.memoization_method} :positional_and_keyword_args
|
113
99
|
|
114
100
|
def positional_and_splat_args(a, *args)
|
115
|
-
100
|
101
|
+
100
|
116
102
|
end
|
117
103
|
#{benchmark_gem.memoization_method} :positional_and_splat_args
|
118
104
|
|
119
105
|
def keyword_and_double_splat_args(a:, **kwargs)
|
120
|
-
100
|
106
|
+
100
|
121
107
|
end
|
122
108
|
#{benchmark_gem.memoization_method} :keyword_and_double_splat_args
|
123
109
|
|
124
110
|
def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
|
125
|
-
100
|
111
|
+
100
|
126
112
|
end
|
127
113
|
#{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
|
128
114
|
end
|
129
|
-
|
130
|
-
# rubocop:enable Security/Eval
|
115
|
+
HEREDOC
|
131
116
|
end
|
132
117
|
|
133
|
-
|
134
|
-
# our benchmarks are running the exact same inputs for each case.
|
135
|
-
N_UNIQUE_ARGUMENTS = 100
|
136
|
-
ARGUMENTS = Array.new(N_UNIQUE_ARGUMENTS) { |i| [i, i + 1] }
|
137
|
-
|
138
|
-
# We benchmark different cases separately, to ensure that slow performance in
|
139
|
-
# one method or code path isn't hidden by fast performance in another.
|
140
|
-
|
141
|
-
Benchmark.ips do |x|
|
142
|
-
x.config(suite: suite)
|
143
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
144
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
118
|
+
N_RESULT_DECIMAL_DIGITS = 2
|
145
119
|
|
146
|
-
|
147
|
-
|
120
|
+
# Each method within these benchmarks is initially run once to memoize the
|
121
|
+
# result value, so our benchmark only tests memoized retrieval time.
|
122
|
+
benchmark_lambdas = [
|
123
|
+
lambda do |x, instance, benchmark_gem|
|
148
124
|
instance.no_args
|
149
125
|
|
150
|
-
x.report("#{benchmark_gem.benchmark_name}: ()
|
126
|
+
x.report("#{benchmark_gem.benchmark_name}: ()") do
|
151
127
|
instance.no_args
|
152
128
|
end
|
153
|
-
end
|
154
|
-
|
155
|
-
|
156
|
-
end
|
157
|
-
|
158
|
-
Benchmark.ips do |x|
|
159
|
-
x.config(suite: suite)
|
160
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
161
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
162
|
-
|
163
|
-
# Run once to memoize the result value, so our benchmark only tests memoized
|
164
|
-
# retrieval time.
|
165
|
-
instance.no_args_falsey
|
166
|
-
|
167
|
-
x.report("#{benchmark_gem.benchmark_name}: () => falsey") do
|
168
|
-
instance.no_args_falsey
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
x.compare!
|
173
|
-
end
|
174
|
-
|
175
|
-
Benchmark.ips do |x|
|
176
|
-
x.config(suite: suite)
|
177
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
178
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
179
|
-
|
180
|
-
# Run once with each set of arguments to memoize the result values, so our
|
181
|
-
# benchmark only tests memoized retrieval time.
|
182
|
-
ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
|
129
|
+
end,
|
130
|
+
lambda do |x, instance, benchmark_gem|
|
131
|
+
instance.one_positional_arg(1)
|
183
132
|
|
184
133
|
x.report("#{benchmark_gem.benchmark_name}: (a)") do
|
185
|
-
|
134
|
+
instance.one_positional_arg(1)
|
186
135
|
end
|
187
|
-
end
|
188
|
-
|
189
|
-
|
190
|
-
end
|
191
|
-
|
192
|
-
Benchmark.ips do |x|
|
193
|
-
x.config(suite: suite)
|
194
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
195
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
196
|
-
|
197
|
-
# Run once with each set of arguments to memoize the result values, so our
|
198
|
-
# benchmark only tests memoized retrieval time.
|
199
|
-
ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
|
136
|
+
end,
|
137
|
+
lambda do |x, instance, benchmark_gem|
|
138
|
+
instance.positional_args(1, 2)
|
200
139
|
|
201
140
|
x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
|
202
|
-
|
141
|
+
instance.positional_args(1, 2)
|
203
142
|
end
|
204
|
-
end
|
205
|
-
|
206
|
-
|
207
|
-
end
|
208
|
-
|
209
|
-
Benchmark.ips do |x|
|
210
|
-
x.config(suite: suite)
|
211
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
212
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
213
|
-
|
214
|
-
# Run once with each set of arguments to memoize the result values, so our
|
215
|
-
# benchmark only tests memoized retrieval time.
|
216
|
-
ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
|
143
|
+
end,
|
144
|
+
lambda do |x, instance, benchmark_gem|
|
145
|
+
instance.one_keyword_arg(a: 1)
|
217
146
|
|
218
147
|
x.report("#{benchmark_gem.benchmark_name}: (a:)") do
|
219
|
-
|
148
|
+
instance.one_keyword_arg(a: 1)
|
220
149
|
end
|
221
|
-
end
|
222
|
-
|
223
|
-
|
224
|
-
end
|
225
|
-
|
226
|
-
Benchmark.ips do |x|
|
227
|
-
x.config(suite: suite)
|
228
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
229
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
230
|
-
|
231
|
-
# Run once with each set of arguments to memoize the result values, so our
|
232
|
-
# benchmark only tests memoized retrieval time.
|
233
|
-
ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
|
150
|
+
end,
|
151
|
+
lambda do |x, instance, benchmark_gem|
|
152
|
+
instance.keyword_args(a: 1, b: 2)
|
234
153
|
|
235
154
|
x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
|
236
|
-
|
155
|
+
instance.keyword_args(a: 1, b: 2)
|
237
156
|
end
|
238
|
-
end
|
239
|
-
|
240
|
-
|
241
|
-
end
|
242
|
-
|
243
|
-
Benchmark.ips do |x|
|
244
|
-
x.config(suite: suite)
|
245
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
246
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
247
|
-
|
248
|
-
# Run once with each set of arguments to memoize the result values, so our
|
249
|
-
# benchmark only tests memoized retrieval time.
|
250
|
-
ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
|
157
|
+
end,
|
158
|
+
lambda do |x, instance, benchmark_gem|
|
159
|
+
instance.positional_and_keyword_args(1, b: 2)
|
251
160
|
|
252
161
|
x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
|
253
|
-
|
162
|
+
instance.positional_and_keyword_args(1, b: 2)
|
254
163
|
end
|
255
|
-
end
|
256
|
-
|
257
|
-
|
258
|
-
end
|
259
|
-
|
260
|
-
Benchmark.ips do |x|
|
261
|
-
x.config(suite: suite)
|
262
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
263
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
264
|
-
|
265
|
-
# Run once with each set of arguments to memoize the result values, so our
|
266
|
-
# benchmark only tests memoized retrieval time.
|
267
|
-
ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
|
164
|
+
end,
|
165
|
+
lambda do |x, instance, benchmark_gem|
|
166
|
+
instance.positional_and_splat_args(1, 2)
|
268
167
|
|
269
168
|
x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
|
270
|
-
|
169
|
+
instance.positional_and_splat_args(1, 2)
|
271
170
|
end
|
272
|
-
end
|
273
|
-
|
274
|
-
|
275
|
-
end
|
171
|
+
end,
|
172
|
+
lambda do |x, instance, benchmark_gem|
|
173
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
276
174
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
# benchmark only tests memoized retrieval time.
|
284
|
-
ARGUMENTS.each { |a, b| instance.keyword_and_double_splat_args(a: a, b: b) }
|
175
|
+
x.report("#{benchmark_gem.benchmark_name}: (a:, **kwargs)") do
|
176
|
+
instance.keyword_and_double_splat_args(a: 1, b: 2)
|
177
|
+
end
|
178
|
+
end,
|
179
|
+
lambda do |x, instance, benchmark_gem|
|
180
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
285
181
|
|
286
|
-
x.report(
|
287
|
-
|
288
|
-
) do
|
289
|
-
ARGUMENTS.each do |a, b|
|
290
|
-
instance.keyword_and_double_splat_args(a: a, b: b)
|
291
|
-
end
|
182
|
+
x.report("#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)") do
|
183
|
+
instance.positional_splat_keyword_and_double_splat_args(1, 2, b: 3, a: 4)
|
292
184
|
end
|
293
185
|
end
|
186
|
+
]
|
294
187
|
|
295
|
-
|
296
|
-
|
188
|
+
# We benchmark different cases separately, to ensure that slow performance in
|
189
|
+
# one method or code path isn't hidden by fast performance in another.
|
190
|
+
benchmark_lambdas.map do |benchmark|
|
191
|
+
json_file = Tempfile.new
|
297
192
|
|
298
|
-
Benchmark.ips do |x|
|
299
|
-
|
300
|
-
|
301
|
-
|
193
|
+
Benchmark.ips do |x|
|
194
|
+
x.config(suite: suite)
|
195
|
+
BENCHMARK_GEMS.each do |benchmark_gem|
|
196
|
+
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
302
197
|
|
303
|
-
|
304
|
-
# benchmark only tests memoized retrieval time.
|
305
|
-
ARGUMENTS.each do |a, b|
|
306
|
-
instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
198
|
+
benchmark.call(x, instance, benchmark_gem)
|
307
199
|
end
|
308
200
|
|
309
|
-
x.
|
310
|
-
|
311
|
-
) do
|
312
|
-
ARGUMENTS.each do |a, b|
|
313
|
-
instance.
|
314
|
-
positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
315
|
-
end
|
316
|
-
end
|
201
|
+
x.compare!
|
202
|
+
x.json! json_file.path
|
317
203
|
end
|
318
204
|
|
319
|
-
|
205
|
+
JSON.parse(json_file.read)
|
206
|
+
end.each_with_index do |benchmark_json, i|
|
207
|
+
# We print a comparison table after we run each benchmark to copy into our
|
208
|
+
# README.md
|
209
|
+
|
210
|
+
# MemoWise will not appear in the comparison table, but we will use it to
|
211
|
+
# compare against other gems' benchmarks
|
212
|
+
memo_wise = benchmark_json.find { _1["name"].include?("MemoWise") }
|
213
|
+
benchmark_json.delete(memo_wise)
|
214
|
+
|
215
|
+
# Sort benchmarks by gem name to alphabetize our final output table.
|
216
|
+
benchmark_json.sort_by! { _1["name"] }
|
217
|
+
|
218
|
+
# Print headers based on the first benchmark_json
|
219
|
+
if i.zero?
|
220
|
+
benchmark_headers = benchmark_json.map do |benchmark_gem|
|
221
|
+
# Gem name is of the form:
|
222
|
+
# "MemoWise (1.1.0): ()"
|
223
|
+
# We use this mapping to get a header of the form
|
224
|
+
# "`MemoWise` (1.1.0)
|
225
|
+
gem_name_parts = benchmark_gem["name"].split
|
226
|
+
"`#{gem_name_parts[0]}` #{gem_name_parts[1][...-1]}"
|
227
|
+
end.join("|")
|
228
|
+
puts "|Method arguments|#{benchmark_headers}|"
|
229
|
+
puts "#{'|--' * (benchmark_json.size + 1)}|"
|
230
|
+
end
|
231
|
+
|
232
|
+
output_str = benchmark_json.map do |bgem|
|
233
|
+
# "%.2f" % 12.345 => "12.34" (instead of "12.35")
|
234
|
+
# See: https://bugs.ruby-lang.org/issues/12548
|
235
|
+
# 1.00.round(2).to_s => "1.0" (instead of "1.00")
|
236
|
+
#
|
237
|
+
# So to round and format correctly, we first use Float#round and then %
|
238
|
+
"%.#{N_RESULT_DECIMAL_DIGITS}fx" %
|
239
|
+
(memo_wise["central_tendency"] / bgem["central_tendency"]).round(N_RESULT_DECIMAL_DIGITS)
|
240
|
+
end.join("|")
|
241
|
+
|
242
|
+
name = memo_wise["name"].partition(": ").last
|
243
|
+
puts "|`#{name}`#{' (none)' if name == '()'}|#{output_str}|"
|
320
244
|
end
|