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.
@@ -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