memo_wise 1.1.0 → 1.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/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
|