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.
@@ -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 if a.even?
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 if a.even?
81
+ 100
96
82
  end
97
83
  #{benchmark_gem.memoization_method} :positional_args
98
84
 
99
85
  def one_keyword_arg(a:)
100
- 100 if a.even?
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 if a.even?
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 if a.even?
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 if a.even?
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 if a.even?
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 if a.even?
111
+ 100
126
112
  end
127
113
  #{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
128
114
  end
129
- CLASS
130
- # rubocop:enable Security/Eval
115
+ HEREDOC
131
116
  end
132
117
 
133
- # We pre-create argument lists for our memoized methods with arguments, so that
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
- # Run once to memoize the result value, so our benchmark only tests memoized
147
- # retrieval time.
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}: () => truthy") do
126
+ x.report("#{benchmark_gem.benchmark_name}: ()") do
151
127
  instance.no_args
152
128
  end
153
- end
154
-
155
- x.compare!
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
- ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
134
+ instance.one_positional_arg(1)
186
135
  end
187
- end
188
-
189
- x.compare!
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
- ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
141
+ instance.positional_args(1, 2)
203
142
  end
204
- end
205
-
206
- x.compare!
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
- ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
148
+ instance.one_keyword_arg(a: 1)
220
149
  end
221
- end
222
-
223
- x.compare!
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
- ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
155
+ instance.keyword_args(a: 1, b: 2)
237
156
  end
238
- end
239
-
240
- x.compare!
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
- ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
162
+ instance.positional_and_keyword_args(1, b: 2)
254
163
  end
255
- end
256
-
257
- x.compare!
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
- ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
169
+ instance.positional_and_splat_args(1, 2)
271
170
  end
272
- end
273
-
274
- x.compare!
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
- Benchmark.ips do |x|
278
- x.config(suite: suite)
279
- BENCHMARK_GEMS.each do |benchmark_gem|
280
- instance = Object.const_get("#{benchmark_gem.klass}Example").new
281
-
282
- # Run once with each set of arguments to memoize the result values, so our
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
- "#{benchmark_gem.benchmark_name}: (a:, **kwargs)"
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
- x.compare!
296
- end
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
- x.config(suite: suite)
300
- BENCHMARK_GEMS.each do |benchmark_gem|
301
- instance = Object.const_get("#{benchmark_gem.klass}Example").new
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
- # Run once with each set of arguments to memoize the result values, so our
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.report(
310
- "#{benchmark_gem.benchmark_name}: (a, *args, b:, **kwargs)"
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
- x.compare!
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