memo_wise 1.1.0 → 1.2.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/.rubocop.yml +12 -0
- data/CHANGELOG.md +18 -5
- data/Gemfile.lock +3 -3
- data/README.md +42 -38
- data/benchmarks/benchmarks.rb +119 -136
- data/lib/memo_wise/internal_api.rb +124 -179
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +140 -126
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a2fbbb2a403502d6a492068a9a0c8c103f717ea5b32e5029ba1a0aebab61db6
|
4
|
+
data.tar.gz: c2c8118caa1621670b6cd666f92147eb5c3f069c7b910062ed0f635568764dd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 699415e3445e60bf6037fdb6887a064a4d082159be15c8c52a0711be5b3b233e3a7caa974f6d31df769d9bd0c3825d7a9f11b17b1e3b98ec9202ee8224801204
|
7
|
+
data.tar.gz: f6f377da39902d30310e7304e0caa7195e372c5fe0666e05ea30d2c2f936f3cc7d00c3588e453d5ab4ba7393a4f747f5c328acd4f4024cfa126ecb5f683fd24c
|
@@ -1,4 +1,4 @@
|
|
1
1
|
**Before merging:**
|
2
2
|
|
3
|
-
- [ ] Copy the latest benchmark results into the `README.md` and update this PR
|
4
|
-
- [ ] If this change merits an update to `CHANGELOG.md`, add an entry following Keep a Changelog [guidelines](https://keepachangelog.com/en/1.0.0/) with [semantic versioning](https://semver.org/)
|
3
|
+
- [ ] Copy the table printed at the end of the latest benchmark results into the `README.md` and update this PR
|
4
|
+
- [ ] If this change merits an update to `CHANGELOG.md`, add an entry following Keep a Changelog [guidelines](https://keepachangelog.com/en/1.0.0/) with [semantic versioning](https://semver.org/)
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
inherit_gem:
|
2
2
|
panolint: rubocop.yml
|
3
3
|
|
4
|
+
Layout/LineLength:
|
5
|
+
Max: 120
|
6
|
+
|
7
|
+
Metrics/ModuleLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/PerceivedComplexity:
|
11
|
+
Enabled: false
|
12
|
+
|
4
13
|
Style/DocumentDynamicEvalDefinition:
|
5
14
|
Enabled: false
|
15
|
+
|
16
|
+
Style/MultipleComparison:
|
17
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -6,24 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
8
|
## Unreleased
|
9
|
+
- Nothing yet!
|
10
|
+
|
11
|
+
## [1.2.0] - 2021-11-10
|
12
|
+
|
9
13
|
### Updated
|
10
|
-
-
|
14
|
+
- Improved performance of all methods by using an outer Array instead of a Hash
|
15
|
+
- Improved performance for multi-argument methods and simplify internal data
|
16
|
+
structures
|
17
|
+
|
11
18
|
### Fixed
|
12
|
-
-
|
19
|
+
- Removed use of #hash due to potential of hash collisions
|
20
|
+
- Updated internal local variable names to avoid name collisions with method
|
21
|
+
arguments
|
22
|
+
|
13
23
|
### Breaking Changes
|
14
24
|
- None
|
15
25
|
|
16
26
|
## [1.1.0] - 2021-07-29
|
17
27
|
### Updated
|
18
|
-
-
|
28
|
+
- Improved performance across the board by:
|
19
29
|
- removing `Hash#fetch`
|
20
30
|
- using `Array#hash`
|
21
31
|
- avoiding multi-layer hash lookups for multi-argument methods
|
22
32
|
- optimizing for truthy results
|
23
33
|
- Add `dry-core` to benchmarks in README
|
34
|
+
|
24
35
|
### Fixed
|
25
|
-
-
|
26
|
-
-
|
36
|
+
- Fixed usage on module singleton classes
|
37
|
+
- Fixed usage on module which would be extended by other classes
|
38
|
+
|
27
39
|
### Breaking Changes
|
28
40
|
- None
|
29
41
|
|
@@ -31,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
31
43
|
### Added
|
32
44
|
- Support for `.preset_memo_wise` on class methods
|
33
45
|
- Support for `.reset_memo_wise` on class methods
|
46
|
+
|
34
47
|
### Updated
|
35
48
|
- Improved performance for common cases by reducing array allocations
|
36
49
|
|
data/Gemfile.lock
CHANGED
@@ -14,7 +14,7 @@ GIT
|
|
14
14
|
PATH
|
15
15
|
remote: .
|
16
16
|
specs:
|
17
|
-
memo_wise (1.
|
17
|
+
memo_wise (1.2.0)
|
18
18
|
|
19
19
|
GEM
|
20
20
|
remote: https://rubygems.org/
|
@@ -27,7 +27,7 @@ GEM
|
|
27
27
|
ansi (1.5.0)
|
28
28
|
ast (2.4.2)
|
29
29
|
brakeman (5.1.1)
|
30
|
-
codecov (0.
|
30
|
+
codecov (0.6.0)
|
31
31
|
simplecov (>= 0.15, < 0.22)
|
32
32
|
concurrent-ruby (1.1.9)
|
33
33
|
diff-lcs (1.4.4)
|
@@ -117,4 +117,4 @@ DEPENDENCIES
|
|
117
117
|
yard-doctest (~> 0.1)
|
118
118
|
|
119
119
|
BUNDLED WITH
|
120
|
-
2.2.
|
120
|
+
2.2.28
|
data/README.md
CHANGED
@@ -56,6 +56,15 @@ class Example
|
|
56
56
|
x
|
57
57
|
end
|
58
58
|
memo_wise :slow_value
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# maintains privacy of the memoized method
|
63
|
+
def private_slow_method(x)
|
64
|
+
sleep x
|
65
|
+
x
|
66
|
+
end
|
67
|
+
memo_wise :private_slow_method
|
59
68
|
end
|
60
69
|
|
61
70
|
ex = Example.new
|
@@ -102,44 +111,38 @@ For more usage details, see our detailed [documentation](#documentation).
|
|
102
111
|
|
103
112
|
## Benchmarks
|
104
113
|
|
105
|
-
Benchmarks
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|`(
|
117
|
-
|`(a)`|
|
118
|
-
|`(a
|
119
|
-
|`(a
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|`(
|
135
|
-
|`(a)`|
|
136
|
-
|`(a, b)`|
|
137
|
-
|`(a:)`|baseline|25.88x|21.74x|17.68x|19.17x|
|
138
|
-
|`(a:, b:)`|baseline|5.72x|4.57x|3.90x|4.42x|
|
139
|
-
|`(a, b:)`|baseline|5.63x|5.06x|4.20x|4.18x|
|
140
|
-
|`(a, *args)`|baseline|5.13x|3.89x|3.25x|3.31x|
|
141
|
-
|`(a:, **kwargs)`|baseline|3.23x|2.55x|2.33x|2.47x|
|
142
|
-
|`(a, *args, b:, **kwargs)`|baseline|2.51x|2.00x|1.84x|1.88x|
|
114
|
+
Benchmarks are run in GitHub Actions, and the tables below are updated with every code change. **Values >1.00x represent how much _slower_ each gem’s memoized value retrieval is than the latest commit of `MemoWise`**, according to [`benchmark-ips`](https://github.com/evanphx/benchmark-ips) (2.9.1).
|
115
|
+
|
116
|
+
Results using Ruby 3.0.2:
|
117
|
+
|
118
|
+
|Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
|
119
|
+
|--|--|--|
|
120
|
+
|`()` (none)|1.51x|19.82x|
|
121
|
+
|`(a)`|2.30x|11.38x|
|
122
|
+
|`(a, b)`|0.45x|2.10x|
|
123
|
+
|`(a:)`|2.20x|22.83x|
|
124
|
+
|`(a:, b:)`|0.49x|4.53x|
|
125
|
+
|`(a, b:)`|0.46x|4.35x|
|
126
|
+
|`(a, *args)`|0.89x|2.03x|
|
127
|
+
|`(a:, **kwargs)`|0.82x|3.18x|
|
128
|
+
|`(a, *args, b:, **kwargs)`|0.60x|1.62x|
|
129
|
+
|
130
|
+
\* `Dry::Core`
|
131
|
+
[may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
|
132
|
+
|
133
|
+
Results using Ruby 2.7.4 (because these gems raise errors in Ruby 3.x):
|
134
|
+
|
135
|
+
|Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
|
136
|
+
|--|--|--|--|--|
|
137
|
+
|`()` (none)|35.29x|3.46x|1.67x|4.27x|
|
138
|
+
|`(a)`|25.04x|16.96x|12.83x|14.68x|
|
139
|
+
|`(a, b)`|3.20x|2.28x|1.84x|2.04x|
|
140
|
+
|`(a:)`|34.17x|27.77x|24.07x|25.39x|
|
141
|
+
|`(a:, b:)`|5.22x|4.29x|3.74x|4.00x|
|
142
|
+
|`(a, b:)`|4.81x|3.99x|3.49x|3.66x|
|
143
|
+
|`(a, *args)`|3.21x|2.30x|1.97x|2.00x|
|
144
|
+
|`(a:, **kwargs)`|2.84x|2.39x|2.12x|2.19x|
|
145
|
+
|`(a, *args, b:, **kwargs)`|2.10x|1.80x|1.67x|1.66x|
|
143
146
|
|
144
147
|
You can run benchmarks yourself with:
|
145
148
|
|
@@ -207,6 +210,7 @@ We've written more about MemoWise in a series of blog posts:
|
|
207
210
|
|
208
211
|
- [Introducing: MemoWise](https://medium.com/building-panorama-education/introducing-memowise-51a5f0523489)
|
209
212
|
- [Optimizing MemoWise Performance](https://ja.cob.land/optimizing-memowise-performance)
|
213
|
+
- [Esosteric Ruby in MemoWise](https://jemma.dev/blog/esoteric-ruby-in-memowise)
|
210
214
|
|
211
215
|
## Logo
|
212
216
|
|
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,7 +59,6 @@ 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.
|
@@ -87,42 +86,42 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
87
86
|
#{benchmark_gem.memoization_method} :no_args_falsey
|
88
87
|
|
89
88
|
def one_positional_arg(a)
|
90
|
-
100 if a.
|
89
|
+
100 if a.positive?
|
91
90
|
end
|
92
91
|
#{benchmark_gem.memoization_method} :one_positional_arg
|
93
92
|
|
94
93
|
def positional_args(a, b)
|
95
|
-
100 if a.
|
94
|
+
100 if a.positive?
|
96
95
|
end
|
97
96
|
#{benchmark_gem.memoization_method} :positional_args
|
98
97
|
|
99
98
|
def one_keyword_arg(a:)
|
100
|
-
100 if a.
|
99
|
+
100 if a.positive?
|
101
100
|
end
|
102
101
|
#{benchmark_gem.memoization_method} :one_keyword_arg
|
103
102
|
|
104
103
|
def keyword_args(a:, b:)
|
105
|
-
100 if a.
|
104
|
+
100 if a.positive?
|
106
105
|
end
|
107
106
|
#{benchmark_gem.memoization_method} :keyword_args
|
108
107
|
|
109
108
|
def positional_and_keyword_args(a, b:)
|
110
|
-
100 if a.
|
109
|
+
100 if a.positive?
|
111
110
|
end
|
112
111
|
#{benchmark_gem.memoization_method} :positional_and_keyword_args
|
113
112
|
|
114
113
|
def positional_and_splat_args(a, *args)
|
115
|
-
100 if a.
|
114
|
+
100 if a.positive?
|
116
115
|
end
|
117
116
|
#{benchmark_gem.memoization_method} :positional_and_splat_args
|
118
117
|
|
119
118
|
def keyword_and_double_splat_args(a:, **kwargs)
|
120
|
-
100 if a.
|
119
|
+
100 if a.positive?
|
121
120
|
end
|
122
121
|
#{benchmark_gem.memoization_method} :keyword_and_double_splat_args
|
123
122
|
|
124
123
|
def positional_splat_keyword_and_double_splat_args(a, *args, b:, **kwargs)
|
125
|
-
100 if a.
|
124
|
+
100 if a.positive?
|
126
125
|
end
|
127
126
|
#{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
|
128
127
|
end
|
@@ -132,155 +131,93 @@ end
|
|
132
131
|
|
133
132
|
# We pre-create argument lists for our memoized methods with arguments, so that
|
134
133
|
# our benchmarks are running the exact same inputs for each case.
|
135
|
-
|
134
|
+
#
|
135
|
+
# NOTE: The proportion of falsey results is 1/N_UNIQUE_ARGUMENTS (because for
|
136
|
+
# the methods with arguments we are truthy for all but the first unique argument
|
137
|
+
# set, and for zero-arity methods we manually execute `no_args` N_TRUTHY_RESULTS
|
138
|
+
# times per each execution of `no_args_falsey`). This number was selected as the
|
139
|
+
# lowest number such that this logic:
|
140
|
+
#
|
141
|
+
# output = hash[key]
|
142
|
+
# if output || hash.key?(key)
|
143
|
+
# output
|
144
|
+
# else
|
145
|
+
# hash[key] = _original_method(...)
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# is consistently faster for cached lookups than:
|
149
|
+
#
|
150
|
+
# hash.fetch(key) do
|
151
|
+
# hash[key] = _original_method(...)
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# as a result of `Hash#[]` having less overhead than `Hash#fetch`.
|
155
|
+
#
|
156
|
+
# We believe this is a reasonable choice because we believe most memoized method
|
157
|
+
# results will be truthy, and so that is the case we should most optimize for.
|
158
|
+
# However, we do not want to completely remove falsey method results from these
|
159
|
+
# benchmarks because we do want to catch performance regressions for that case,
|
160
|
+
# since it has its own "hot path."
|
161
|
+
N_UNIQUE_ARGUMENTS = 30
|
136
162
|
ARGUMENTS = Array.new(N_UNIQUE_ARGUMENTS) { |i| [i, i + 1] }
|
163
|
+
N_TRUTHY_RESULTS = N_UNIQUE_ARGUMENTS - 1
|
164
|
+
N_RESULT_DECIMAL_DIGITS = 2
|
137
165
|
|
138
|
-
#
|
139
|
-
#
|
140
|
-
|
141
|
-
|
142
|
-
x.config(suite: suite)
|
143
|
-
BENCHMARK_GEMS.each do |benchmark_gem|
|
144
|
-
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
145
|
-
|
146
|
-
# Run once to memoize the result value, so our benchmark only tests memoized
|
147
|
-
# retrieval time.
|
148
|
-
instance.no_args
|
149
|
-
|
150
|
-
x.report("#{benchmark_gem.benchmark_name}: () => truthy") do
|
151
|
-
instance.no_args
|
152
|
-
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.
|
166
|
+
# Each method within these benchmarks is initially run once to memoize the
|
167
|
+
# result value, so our benchmark only tests memoized retrieval time.
|
168
|
+
benchmark_lambdas = [
|
169
|
+
lambda do |x, instance, benchmark_gem|
|
165
170
|
instance.no_args_falsey
|
171
|
+
instance.no_args
|
166
172
|
|
167
|
-
x.report("#{benchmark_gem.benchmark_name}: ()
|
173
|
+
x.report("#{benchmark_gem.benchmark_name}: ()") do
|
168
174
|
instance.no_args_falsey
|
175
|
+
N_TRUTHY_RESULTS.times { instance.no_args }
|
169
176
|
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.
|
177
|
+
end,
|
178
|
+
lambda do |x, instance, benchmark_gem|
|
182
179
|
ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
|
183
180
|
|
184
181
|
x.report("#{benchmark_gem.benchmark_name}: (a)") do
|
185
182
|
ARGUMENTS.each { |a, _| instance.one_positional_arg(a) }
|
186
183
|
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.
|
184
|
+
end,
|
185
|
+
lambda do |x, instance, benchmark_gem|
|
199
186
|
ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
|
200
187
|
|
201
188
|
x.report("#{benchmark_gem.benchmark_name}: (a, b)") do
|
202
189
|
ARGUMENTS.each { |a, b| instance.positional_args(a, b) }
|
203
190
|
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.
|
191
|
+
end,
|
192
|
+
lambda do |x, instance, benchmark_gem|
|
216
193
|
ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
|
217
194
|
|
218
195
|
x.report("#{benchmark_gem.benchmark_name}: (a:)") do
|
219
196
|
ARGUMENTS.each { |a, _| instance.one_keyword_arg(a: a) }
|
220
197
|
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.
|
198
|
+
end,
|
199
|
+
lambda do |x, instance, benchmark_gem|
|
233
200
|
ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
|
234
201
|
|
235
202
|
x.report("#{benchmark_gem.benchmark_name}: (a:, b:)") do
|
236
203
|
ARGUMENTS.each { |a, b| instance.keyword_args(a: a, b: b) }
|
237
204
|
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.
|
205
|
+
end,
|
206
|
+
lambda do |x, instance, benchmark_gem|
|
250
207
|
ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
|
251
208
|
|
252
209
|
x.report("#{benchmark_gem.benchmark_name}: (a, b:)") do
|
253
210
|
ARGUMENTS.each { |a, b| instance.positional_and_keyword_args(a, b: b) }
|
254
211
|
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.
|
212
|
+
end,
|
213
|
+
lambda do |x, instance, benchmark_gem|
|
267
214
|
ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
|
268
215
|
|
269
216
|
x.report("#{benchmark_gem.benchmark_name}: (a, *args)") do
|
270
217
|
ARGUMENTS.each { |a, b| instance.positional_and_splat_args(a, b) }
|
271
218
|
end
|
272
|
-
end
|
273
|
-
|
274
|
-
x.compare!
|
275
|
-
end
|
276
|
-
|
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.
|
219
|
+
end,
|
220
|
+
lambda do |x, instance, benchmark_gem|
|
284
221
|
ARGUMENTS.each { |a, b| instance.keyword_and_double_splat_args(a: a, b: b) }
|
285
222
|
|
286
223
|
x.report(
|
@@ -290,18 +227,8 @@ Benchmark.ips do |x|
|
|
290
227
|
instance.keyword_and_double_splat_args(a: a, b: b)
|
291
228
|
end
|
292
229
|
end
|
293
|
-
end
|
294
|
-
|
295
|
-
x.compare!
|
296
|
-
end
|
297
|
-
|
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
|
302
|
-
|
303
|
-
# Run once with each set of arguments to memoize the result values, so our
|
304
|
-
# benchmark only tests memoized retrieval time.
|
230
|
+
end,
|
231
|
+
lambda do |x, instance, benchmark_gem|
|
305
232
|
ARGUMENTS.each do |a, b|
|
306
233
|
instance.positional_splat_keyword_and_double_splat_args(a, b, b: b, a: a)
|
307
234
|
end
|
@@ -315,6 +242,62 @@ Benchmark.ips do |x|
|
|
315
242
|
end
|
316
243
|
end
|
317
244
|
end
|
245
|
+
]
|
246
|
+
|
247
|
+
# We benchmark different cases separately, to ensure that slow performance in
|
248
|
+
# one method or code path isn't hidden by fast performance in another.
|
249
|
+
benchmark_lambdas.map do |benchmark|
|
250
|
+
json_file = Tempfile.new
|
251
|
+
|
252
|
+
Benchmark.ips do |x|
|
253
|
+
x.config(suite: suite)
|
254
|
+
BENCHMARK_GEMS.each do |benchmark_gem|
|
255
|
+
instance = Object.const_get("#{benchmark_gem.klass}Example").new
|
256
|
+
|
257
|
+
benchmark.call(x, instance, benchmark_gem)
|
258
|
+
end
|
259
|
+
|
260
|
+
x.compare!
|
261
|
+
x.json! json_file.path
|
262
|
+
end
|
263
|
+
|
264
|
+
JSON.parse(json_file.read)
|
265
|
+
end.each_with_index do |benchmark_json, i|
|
266
|
+
# We print a comparison table after we run each benchmark to copy into our
|
267
|
+
# README.md
|
268
|
+
|
269
|
+
# MemoWise will not appear in the comparison table, but we will use it to
|
270
|
+
# compare against other gems' benchmarks
|
271
|
+
memo_wise = benchmark_json.find { _1["name"].include?("MemoWise") }
|
272
|
+
benchmark_json.delete(memo_wise)
|
273
|
+
|
274
|
+
# Sort benchmarks by gem name to alphabetize our final output table.
|
275
|
+
benchmark_json.sort_by! { _1["name"] }
|
276
|
+
|
277
|
+
# Print headers based on the first benchmark_json
|
278
|
+
if i.zero?
|
279
|
+
benchmark_headers = benchmark_json.map do |benchmark_gem|
|
280
|
+
# Gem name is of the form:
|
281
|
+
# "MemoWise (1.1.0): ()"
|
282
|
+
# We use this mapping to get a header of the form
|
283
|
+
# "`MemoWise` (1.1.0)
|
284
|
+
gem_name_parts = benchmark_gem["name"].split
|
285
|
+
"`#{gem_name_parts[0]}` #{gem_name_parts[1][...-1]}"
|
286
|
+
end.join("|")
|
287
|
+
puts "|Method arguments|#{benchmark_headers}|"
|
288
|
+
puts "#{'|--' * (benchmark_json.size + 1)}|"
|
289
|
+
end
|
290
|
+
|
291
|
+
output_str = benchmark_json.map do |bgem|
|
292
|
+
# "%.2f" % 12.345 => "12.34" (instead of "12.35")
|
293
|
+
# See: https://bugs.ruby-lang.org/issues/12548
|
294
|
+
# 1.00.round(2).to_s => "1.0" (instead of "1.00")
|
295
|
+
#
|
296
|
+
# So to round and format correctly, we first use Float#round and then %
|
297
|
+
"%.#{N_RESULT_DECIMAL_DIGITS}fx" %
|
298
|
+
(memo_wise["central_tendency"] / bgem["central_tendency"]).round(N_RESULT_DECIMAL_DIGITS)
|
299
|
+
end.join("|")
|
318
300
|
|
319
|
-
|
301
|
+
name = memo_wise["name"].partition(": ").last
|
302
|
+
puts "|`#{name}`#{' (none)' if name == '()'}|#{output_str}|"
|
320
303
|
end
|
@@ -10,138 +10,138 @@ module MemoWise
|
|
10
10
|
#
|
11
11
|
# @return [Object] the passed-in obj
|
12
12
|
def self.create_memo_wise_state!(obj)
|
13
|
-
# `@_memo_wise` stores memoized results of method calls.
|
14
|
-
#
|
15
|
-
#
|
13
|
+
# `@_memo_wise` stores memoized results of method calls. The structure is
|
14
|
+
# slightly different for different types of methods. It looks like:
|
15
|
+
# [
|
16
|
+
# :memoized_result, # For method 0 (which takes no arguments)
|
17
|
+
# { arg1 => :memoized_result, ... }, # For method 1 (which takes an argument)
|
18
|
+
# { [arg1, arg2] => :memoized_result, ... } # For method 2 (which takes multiple arguments)
|
19
|
+
# ]
|
20
|
+
# This is a faster alternative to:
|
16
21
|
# {
|
17
|
-
#
|
22
|
+
# zero_arg_method_name: :memoized_result,
|
18
23
|
# single_arg_method_name: { arg1 => :memoized_result, ... },
|
19
|
-
#
|
24
|
+
#
|
25
|
+
# # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2]
|
26
|
+
# multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... }
|
20
27
|
# }
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
28
|
+
# because we can give each method its own array index at load time and
|
29
|
+
# perform that array lookup more quickly than a hash lookup by method
|
30
|
+
# name.
|
31
|
+
obj.instance_variable_set(:@_memo_wise, []) unless obj.instance_variable_defined?(:@_memo_wise)
|
32
|
+
|
33
|
+
# For zero-arity methods, memoized values are stored in the `@_memo_wise`
|
34
|
+
# array. Arrays do not differentiate between "unset" and "set to nil" and
|
35
|
+
# so to handle this case we need another array to store sentinels and
|
36
|
+
# store `true` at indexes for which a zero-arity method has been memoized.
|
37
|
+
# `@_memo_wise_sentinels` looks like:
|
38
|
+
# [
|
39
|
+
# true, # A zero-arity method's result has been memoized
|
40
|
+
# nil, # A zero-arity method's result has not been memoized
|
41
|
+
# nil, # A one-arity method will always correspond to `nil` here
|
35
42
|
# ...
|
36
|
-
#
|
37
|
-
|
38
|
-
|
43
|
+
# ]
|
44
|
+
# NOTE: Because `@_memo_wise` stores memoized values for more than just
|
45
|
+
# zero-arity methods, the `@_memo_wise_sentinels` array can end up being
|
46
|
+
# sparse (see above), even when all methods' memoized values have been
|
47
|
+
# stored. If this becomes an issue we could store a separate index for
|
48
|
+
# zero-arity methods to make every element in `@_memo_wise_sentinels`
|
49
|
+
# correspond to a zero-arity method.
|
50
|
+
# NOTE: Surprisingly, lookups on an array of `true` and `nil` values
|
51
|
+
# appear to outperform even bitwise operators on integers (as of Ruby
|
52
|
+
# 3.0.2), allowing us to avoid more complex sentinel structures.
|
53
|
+
unless obj.instance_variable_defined?(:@_memo_wise_sentinels)
|
54
|
+
obj.instance_variable_set(:@_memo_wise_sentinels, [])
|
39
55
|
end
|
40
56
|
|
41
57
|
obj
|
42
58
|
end
|
43
59
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
# @param method [
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
60
|
+
NONE = :none
|
61
|
+
ONE_REQUIRED_POSITIONAL = :one_required_positional
|
62
|
+
ONE_REQUIRED_KEYWORD = :one_required_keyword
|
63
|
+
MULTIPLE_REQUIRED = :multiple_required
|
64
|
+
SPLAT = :splat
|
65
|
+
DOUBLE_SPLAT = :double_splat
|
66
|
+
SPLAT_AND_DOUBLE_SPLAT = :splat_and_double_splat
|
67
|
+
|
68
|
+
# @param method [UnboundMethod] a method to categorize based on the types of
|
69
|
+
# arguments it has
|
70
|
+
# @return [Symbol] one of:
|
71
|
+
# - :none (example: `def foo`)
|
72
|
+
# - :one_required_positional (example: `def foo(a)`)
|
73
|
+
# - :one_required_keyword (example: `def foo(a:)`)
|
74
|
+
# - :multiple_required (examples: `def foo(a, b)`, `def foo(a:, b:)`, `def foo(a, b:)`)
|
75
|
+
# - :splat (examples: `def foo(a=1)`, `def foo(a, *b)`)
|
76
|
+
# - :double_splat (examples: `def foo(a: 1)`, `def foo(a:, **b)`)
|
77
|
+
# - :splat_and_double_splat (examples: `def foo(a=1, b: 2)`, `def foo(a=1, **b)`, `def foo(*a, **b)`)
|
78
|
+
def self.method_arguments(method)
|
79
|
+
return NONE if method.arity.zero?
|
80
|
+
|
81
|
+
parameters = method.parameters.map(&:first)
|
82
|
+
|
83
|
+
if parameters == [:req]
|
84
|
+
ONE_REQUIRED_POSITIONAL
|
85
|
+
elsif parameters == [:keyreq]
|
86
|
+
ONE_REQUIRED_KEYWORD
|
87
|
+
elsif parameters.all? { |type| type == :req || type == :keyreq }
|
88
|
+
MULTIPLE_REQUIRED
|
89
|
+
elsif parameters & %i[req opt rest] == parameters.uniq
|
90
|
+
SPLAT
|
91
|
+
elsif parameters & %i[keyreq key keyrest] == parameters.uniq
|
92
|
+
DOUBLE_SPLAT
|
93
|
+
else
|
94
|
+
SPLAT_AND_DOUBLE_SPLAT
|
76
95
|
end
|
77
96
|
end
|
78
97
|
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# @example
|
94
|
-
# class Example
|
95
|
-
# def position_args(a, b=1)
|
96
|
-
# end
|
97
|
-
#
|
98
|
-
# def keyword_args(a:, b: 1)
|
99
|
-
# end
|
100
|
-
# end
|
101
|
-
#
|
102
|
-
# MemoWise::InternalAPI.
|
103
|
-
# has_kwarg?(Example.instance_method(:position_args)) #=> false
|
104
|
-
#
|
105
|
-
# MemoWise::InternalAPI.
|
106
|
-
# has_kwarg?(Example.instance_method(:keyword_args)) #=> true
|
107
|
-
#
|
108
|
-
def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
|
109
|
-
method.parameters.any? do |param, _|
|
110
|
-
param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
|
98
|
+
# @param method [UnboundMethod] a method being memoized
|
99
|
+
# @return [String] the arguments string to use when defining our new
|
100
|
+
# memoized version of the method
|
101
|
+
def self.args_str(method)
|
102
|
+
case method_arguments(method)
|
103
|
+
when SPLAT then "*args"
|
104
|
+
when DOUBLE_SPLAT then "**kwargs"
|
105
|
+
when SPLAT_AND_DOUBLE_SPLAT then "*args, **kwargs"
|
106
|
+
when ONE_REQUIRED_POSITIONAL, ONE_REQUIRED_KEYWORD, MULTIPLE_REQUIRED
|
107
|
+
method.parameters.map do |type, name|
|
108
|
+
"#{name}#{':' if type == :keyreq}"
|
109
|
+
end.join(", ")
|
110
|
+
else
|
111
|
+
raise ArgumentError, "Unexpected arguments for #{method.name}"
|
111
112
|
end
|
112
113
|
end
|
113
114
|
|
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
|
-
method.parameters.all? { |type, _| type == :req || type == :keyreq } # rubocop:disable Style/MultipleComparison
|
115
|
+
# @param method [UnboundMethod] a method being memoized
|
116
|
+
# @return [String] the arguments string to use when calling the original
|
117
|
+
# method in our new memoized version of the method, i.e. when setting a
|
118
|
+
# memoized value
|
119
|
+
def self.call_str(method)
|
120
|
+
case method_arguments(method)
|
121
|
+
when SPLAT then "*args"
|
122
|
+
when DOUBLE_SPLAT then "**kwargs"
|
123
|
+
when SPLAT_AND_DOUBLE_SPLAT then "*args, **kwargs"
|
124
|
+
when ONE_REQUIRED_POSITIONAL, ONE_REQUIRED_KEYWORD, MULTIPLE_REQUIRED
|
125
|
+
method.parameters.map do |type, name|
|
126
|
+
type == :req ? name : "#{name}: #{name}"
|
127
|
+
end.join(", ")
|
128
|
+
else
|
129
|
+
raise ArgumentError, "Unexpected arguments for #{method.name}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @param method [UnboundMethod] a method being memoized
|
134
|
+
# @return [String] the string to use as a hash key when looking up a
|
135
|
+
# memoized value, based on the method's arguments
|
136
|
+
def self.key_str(method)
|
137
|
+
case method_arguments(method)
|
138
|
+
when SPLAT then "args"
|
139
|
+
when DOUBLE_SPLAT then "kwargs"
|
140
|
+
when SPLAT_AND_DOUBLE_SPLAT then "[args, kwargs]"
|
141
|
+
when MULTIPLE_REQUIRED then "[#{method.parameters.map(&:last).join(', ')}]"
|
142
|
+
else
|
143
|
+
raise ArgumentError, "Unexpected arguments for #{method.name}"
|
144
|
+
end
|
145
145
|
end
|
146
146
|
|
147
147
|
# Find the original class for which the given class is the corresponding
|
@@ -159,9 +159,7 @@ module MemoWise
|
|
159
159
|
# Raises if `klass` is not a singleton class.
|
160
160
|
#
|
161
161
|
def self.original_class_from_singleton(klass)
|
162
|
-
unless klass.singleton_class?
|
163
|
-
raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
|
164
|
-
end
|
162
|
+
raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class?
|
165
163
|
|
166
164
|
# Search ObjectSpace
|
167
165
|
# * 1:1 relationship of singleton class to original class is documented
|
@@ -195,63 +193,11 @@ module MemoWise
|
|
195
193
|
# @return [Class, Module]
|
196
194
|
attr_reader :target
|
197
195
|
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
# Name of method to derive the "fetch key" for, with given parameters.
|
204
|
-
# @param args [Array]
|
205
|
-
# Zero or more positional parameters
|
206
|
-
# @param kwargs [Hash]
|
207
|
-
# Zero or more keyword parameters
|
208
|
-
#
|
209
|
-
# @return [Array, Hash, Object]
|
210
|
-
# Returns one of:
|
211
|
-
# - An `Array` if only positional parameters.
|
212
|
-
# - A nested `Array<Array, Hash>` if *both* positional and keyword.
|
213
|
-
# - A `Hash` if only keyword parameters.
|
214
|
-
# - A single object if there is only a single parameter.
|
215
|
-
def fetch_key(method_name, *args, **kwargs)
|
216
|
-
method = target_class.instance_method(method_name)
|
217
|
-
|
218
|
-
if MemoWise::InternalAPI.has_only_required_args?(method)
|
219
|
-
key = method.parameters.map.with_index do |(type, name), index|
|
220
|
-
type == :req ? args[index] : kwargs[name]
|
221
|
-
end
|
222
|
-
key.size == 1 ? key.first : [method_name, *key].hash
|
223
|
-
else
|
224
|
-
has_arg = MemoWise::InternalAPI.has_arg?(method)
|
225
|
-
|
226
|
-
if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
|
227
|
-
[method_name, args, kwargs].hash
|
228
|
-
elsif has_arg
|
229
|
-
args.hash
|
230
|
-
else
|
231
|
-
kwargs.hash
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
# Returns whether the given method should use an array's hash value as the
|
237
|
-
# cache lookup key. See the comments in `.create_memo_wise_state!` for an
|
238
|
-
# example.
|
239
|
-
#
|
240
|
-
# @param method_name [Symbol]
|
241
|
-
# Name of memoized method we're checking the implementation of
|
242
|
-
#
|
243
|
-
# @return [Boolean] true iff the method uses a hashed cache key; false
|
244
|
-
# otherwise
|
245
|
-
def use_hashed_key?(method_name)
|
246
|
-
method = target_class.instance_method(method_name)
|
247
|
-
|
248
|
-
if MemoWise::InternalAPI.has_arg?(method) &&
|
249
|
-
MemoWise::InternalAPI.has_kwarg?(method)
|
250
|
-
return true
|
251
|
-
end
|
252
|
-
|
253
|
-
MemoWise::InternalAPI.has_only_required_args?(method) &&
|
254
|
-
method.parameters.size > 1
|
196
|
+
# @param method_name [Symbol] the name of the memoized method
|
197
|
+
# @return [Integer] the array index in `@_memo_wise_indices` to use to find
|
198
|
+
# the memoization data for the given method
|
199
|
+
def index(method_name)
|
200
|
+
target_class.instance_variable_get(:@_memo_wise_indices)[method_name]
|
255
201
|
end
|
256
202
|
|
257
203
|
# Returns visibility of an instance method defined on class `target`.
|
@@ -274,8 +220,7 @@ module MemoWise
|
|
274
220
|
elsif target.public_method_defined?(method_name)
|
275
221
|
:public
|
276
222
|
else
|
277
|
-
raise ArgumentError,
|
278
|
-
"#{method_name.inspect} must be a method on #{target}"
|
223
|
+
raise ArgumentError, "#{method_name.inspect} must be a method on #{target}"
|
279
224
|
end
|
280
225
|
end
|
281
226
|
|
data/lib/memo_wise/version.rb
CHANGED
data/lib/memo_wise.rb
CHANGED
@@ -25,7 +25,7 @@ require "memo_wise/version"
|
|
25
25
|
# - {.memo_wise} for API and usage examples.
|
26
26
|
# - {file:README.md} for general project information.
|
27
27
|
#
|
28
|
-
module MemoWise
|
28
|
+
module MemoWise
|
29
29
|
# Constructor to set up memoization state before
|
30
30
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
31
31
|
# constructor.
|
@@ -91,7 +91,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
91
91
|
# prepend MemoWise
|
92
92
|
# end
|
93
93
|
#
|
94
|
-
def self.prepended(target)
|
94
|
+
def self.prepended(target)
|
95
95
|
class << target
|
96
96
|
# Allocator to set up memoization state before
|
97
97
|
# [calling the original](https://medium.com/@jeremy_96642/ruby-method-auditing-using-module-prepend-4f4e69aacd95)
|
@@ -111,7 +111,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
111
111
|
end
|
112
112
|
|
113
113
|
# NOTE: See YARD docs for {.memo_wise} directly below this method!
|
114
|
-
def memo_wise(method_name_or_hash)
|
114
|
+
def memo_wise(method_name_or_hash)
|
115
115
|
klass = self
|
116
116
|
case method_name_or_hash
|
117
117
|
when Symbol
|
@@ -156,100 +156,87 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
156
156
|
klass = klass.singleton_class
|
157
157
|
end
|
158
158
|
|
159
|
-
unless method_name.is_a?(Symbol)
|
160
|
-
raise ArgumentError, "#{method_name.inspect} must be a Symbol"
|
161
|
-
end
|
159
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
162
160
|
|
163
161
|
api = MemoWise::InternalAPI.new(klass)
|
164
162
|
visibility = api.method_visibility(method_name)
|
165
|
-
original_memo_wised_name =
|
166
|
-
MemoWise::InternalAPI.original_memo_wised_name(method_name)
|
163
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(method_name)
|
167
164
|
method = klass.instance_method(method_name)
|
168
165
|
|
169
166
|
klass.send(:alias_method, original_memo_wised_name, method_name)
|
170
167
|
klass.send(:private, original_memo_wised_name)
|
171
168
|
|
172
|
-
|
173
|
-
#
|
174
|
-
|
169
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
170
|
+
# `@_memo_wise_indices` stores the `@_memo_wise` indices of different
|
171
|
+
# method names. We only use this data structure when resetting or
|
172
|
+
# presetting memoization. It looks like:
|
173
|
+
# {
|
174
|
+
# single_arg_method_name: 0,
|
175
|
+
# other_single_arg_method_name: 1
|
176
|
+
# }
|
177
|
+
memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
|
178
|
+
memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
|
179
|
+
index = klass.instance_variable_get(:@_memo_wise_index_counter) || 0
|
180
|
+
|
181
|
+
memo_wise_indices[method_name] = index
|
182
|
+
klass.instance_variable_set(:@_memo_wise_index_counter, index + 1)
|
183
|
+
|
184
|
+
case method_arguments
|
185
|
+
when MemoWise::InternalAPI::NONE
|
186
|
+
# Zero-arg methods can use simpler/more performant logic because the
|
187
|
+
# hash key is just the method name.
|
175
188
|
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
176
189
|
def #{method_name}
|
177
|
-
|
178
|
-
if
|
179
|
-
|
190
|
+
_memo_wise_output = @_memo_wise[#{index}]
|
191
|
+
if _memo_wise_output || @_memo_wise_sentinels[#{index}]
|
192
|
+
_memo_wise_output
|
180
193
|
else
|
181
|
-
@
|
194
|
+
@_memo_wise_sentinels[#{index}] = true
|
195
|
+
@_memo_wise[#{index}] = #{original_memo_wised_name}
|
182
196
|
end
|
183
197
|
end
|
184
198
|
END_OF_METHOD
|
185
|
-
|
186
|
-
|
187
|
-
args_str = method.parameters.map do |type, name|
|
188
|
-
"#{name}#{':' if type == :keyreq}"
|
189
|
-
end.join(", ")
|
190
|
-
args_str = "(#{args_str})"
|
191
|
-
call_str = method.parameters.map do |type, name|
|
192
|
-
type == :req ? name : "#{name}: #{name}"
|
193
|
-
end.join(", ")
|
194
|
-
call_str = "(#{call_str})"
|
195
|
-
fetch_key_params = method.parameters.map(&:last)
|
196
|
-
if fetch_key_params.size > 1
|
197
|
-
fetch_key_init =
|
198
|
-
"[:#{method_name}, #{fetch_key_params.join(', ')}].hash"
|
199
|
-
use_hashed_key = true
|
200
|
-
else
|
201
|
-
fetch_key = fetch_key_params.first.to_s
|
202
|
-
end
|
203
|
-
else
|
204
|
-
# If our method has arguments, we need to separate out our handling
|
205
|
-
# of normal args vs. keyword args due to the changes in Ruby 3.
|
206
|
-
# See: <link>
|
207
|
-
# By only including logic for *args, **kwargs when they are used in
|
208
|
-
# the method, we can avoid allocating unnecessary arrays and hashes.
|
209
|
-
has_arg = MemoWise::InternalAPI.has_arg?(method)
|
210
|
-
|
211
|
-
if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
|
212
|
-
args_str = "(*args, **kwargs)"
|
213
|
-
fetch_key_init = "[:#{method_name}, args, kwargs].hash"
|
214
|
-
use_hashed_key = true
|
215
|
-
elsif has_arg
|
216
|
-
args_str = "(*args)"
|
217
|
-
fetch_key_init = "args.hash"
|
218
|
-
else
|
219
|
-
args_str = "(**kwargs)"
|
220
|
-
fetch_key_init = "kwargs.hash"
|
221
|
-
end
|
222
|
-
end
|
199
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
200
|
+
key = method.parameters.first.last
|
223
201
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
hashes = (@_memo_wise_hashes[:#{method_name}] ||= Set.new)
|
233
|
-
hashes << key
|
234
|
-
@_memo_wise[key] = #{original_memo_wised_name}#{call_str || args_str}
|
235
|
-
end
|
202
|
+
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
203
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
204
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
205
|
+
_memo_wise_output = _memo_wise_hash[#{key}]
|
206
|
+
if _memo_wise_output || _memo_wise_hash.key?(#{key})
|
207
|
+
_memo_wise_output
|
208
|
+
else
|
209
|
+
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
236
210
|
end
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
211
|
+
end
|
212
|
+
END_OF_METHOD
|
213
|
+
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
|
214
|
+
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
215
|
+
else
|
216
|
+
# NOTE: When benchmarking this implementation against something like:
|
217
|
+
#
|
218
|
+
# @_memo_wise.fetch(key) do
|
219
|
+
# ...
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# this implementation may sometimes perform worse than the above. This
|
223
|
+
# is because this case uses a more complex hash key (see
|
224
|
+
# `MemoWise::InternalAPI.key_str`), and hashing that key has less
|
225
|
+
# consistent performance. In general, this should still be faster for
|
226
|
+
# truthy results because `Hash#[]` generally performs hash lookups
|
227
|
+
# faster than `Hash#fetch`.
|
228
|
+
klass.module_eval <<-END_OF_METHOD, __FILE__, __LINE__ + 1
|
229
|
+
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
230
|
+
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
231
|
+
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
232
|
+
_memo_wise_output = _memo_wise_hash[_memo_wise_key]
|
233
|
+
if _memo_wise_output || _memo_wise_hash.key?(_memo_wise_key)
|
234
|
+
_memo_wise_output
|
235
|
+
else
|
236
|
+
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
250
237
|
end
|
251
|
-
|
252
|
-
|
238
|
+
end
|
239
|
+
END_OF_METHOD
|
253
240
|
end
|
254
241
|
|
255
242
|
klass.send(visibility, method_name)
|
@@ -275,8 +262,7 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
275
262
|
# (`...` or `*args, **kwargs`), making reflection on method parameters
|
276
263
|
# useless without this.
|
277
264
|
def target.instance_method(symbol)
|
278
|
-
original_memo_wised_name =
|
279
|
-
MemoWise::InternalAPI.original_memo_wised_name(symbol)
|
265
|
+
original_memo_wised_name = MemoWise::InternalAPI.original_memo_wised_name(symbol)
|
280
266
|
|
281
267
|
super.tap do |curr_method|
|
282
268
|
# Start with calling the original `instance_method` on `symbol`,
|
@@ -368,7 +354,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
368
354
|
# Example.method_called_times #=> nil
|
369
355
|
##
|
370
356
|
|
371
|
-
# rubocop:disable Layout/LineLength
|
372
357
|
##
|
373
358
|
# @!method self.reset_memo_wise(method_name = nil, *args, **kwargs)
|
374
359
|
# Implementation of {#reset_memo_wise} for class methods.
|
@@ -402,7 +387,6 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
402
387
|
#
|
403
388
|
# Example.reset_memo_wise # reset "all methods" mode
|
404
389
|
##
|
405
|
-
# rubocop:enable Layout/LineLength
|
406
390
|
|
407
391
|
# Presets the memoized result for the given method to the result of the given
|
408
392
|
# block.
|
@@ -455,26 +439,35 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
455
439
|
# ex.method_called_times #=> nil
|
456
440
|
#
|
457
441
|
def preset_memo_wise(method_name, *args, **kwargs)
|
458
|
-
unless block_given?
|
459
|
-
raise ArgumentError,
|
460
|
-
"Pass a block as the value to preset for #{method_name}, #{args}"
|
461
|
-
end
|
442
|
+
raise ArgumentError, "Pass a block as the value to preset for #{method_name}, #{args}" unless block_given?
|
462
443
|
|
463
444
|
api = MemoWise::InternalAPI.new(self)
|
464
445
|
api.validate_memo_wised!(method_name)
|
465
446
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
447
|
+
method = method(method_name)
|
448
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
449
|
+
index = api.index(method_name)
|
450
|
+
|
451
|
+
if method_arguments == MemoWise::InternalAPI::NONE
|
452
|
+
@_memo_wise_sentinels[index] = true
|
453
|
+
@_memo_wise[index] = yield
|
454
|
+
return
|
455
|
+
end
|
456
|
+
|
457
|
+
hash = (@_memo_wise[index] ||= {})
|
458
|
+
|
459
|
+
case method_arguments
|
460
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL then hash[args.first] = yield
|
461
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD then hash[kwargs.first.last] = yield
|
462
|
+
when MemoWise::InternalAPI::SPLAT then hash[args] = yield
|
463
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT then hash[kwargs] = yield
|
464
|
+
when MemoWise::InternalAPI::MULTIPLE_REQUIRED
|
465
|
+
key = method.parameters.map.with_index do |(type, name), idx|
|
466
|
+
type == :req ? args[idx] : kwargs[name]
|
477
467
|
end
|
468
|
+
hash[key] = yield
|
469
|
+
else # MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
470
|
+
hash[[args, kwargs]] = yield
|
478
471
|
end
|
479
472
|
end
|
480
473
|
|
@@ -543,45 +536,66 @@ module MemoWise # rubocop:disable Metrics/ModuleLength
|
|
543
536
|
#
|
544
537
|
# ex.reset_memo_wise # reset "all methods" mode
|
545
538
|
#
|
546
|
-
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
539
|
+
def reset_memo_wise(method_name = nil, *args, **kwargs)
|
547
540
|
if method_name.nil?
|
548
|
-
unless args.empty?
|
549
|
-
|
550
|
-
end
|
551
|
-
|
552
|
-
unless kwargs.empty?
|
553
|
-
raise ArgumentError, "Provided kwargs when method_name = nil"
|
554
|
-
end
|
541
|
+
raise ArgumentError, "Provided args when method_name = nil" unless args.empty?
|
542
|
+
raise ArgumentError, "Provided kwargs when method_name = nil" unless kwargs.empty?
|
555
543
|
|
556
544
|
@_memo_wise.clear
|
557
|
-
@
|
545
|
+
@_memo_wise_sentinels.clear
|
558
546
|
return
|
559
547
|
end
|
560
548
|
|
561
|
-
unless method_name.is_a?(Symbol)
|
562
|
-
|
563
|
-
end
|
564
|
-
|
565
|
-
unless respond_to?(method_name, true)
|
566
|
-
raise ArgumentError, "#{method_name} is not a defined method"
|
567
|
-
end
|
549
|
+
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
550
|
+
raise ArgumentError, "#{method_name} is not a defined method" unless respond_to?(method_name, true)
|
568
551
|
|
569
552
|
api = MemoWise::InternalAPI.new(self)
|
570
553
|
api.validate_memo_wised!(method_name)
|
571
554
|
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
555
|
+
method = method(method_name)
|
556
|
+
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
557
|
+
index = api.index(method_name)
|
558
|
+
|
559
|
+
case method_arguments
|
560
|
+
when MemoWise::InternalAPI::NONE
|
561
|
+
@_memo_wise_sentinels[index] = nil
|
562
|
+
@_memo_wise[index] = nil
|
563
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL
|
564
|
+
if args.empty?
|
565
|
+
@_memo_wise[index]&.clear
|
566
|
+
else
|
567
|
+
@_memo_wise[index]&.delete(args.first)
|
568
|
+
end
|
569
|
+
when MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
570
|
+
if kwargs.empty?
|
571
|
+
@_memo_wise[index]&.clear
|
572
|
+
else
|
573
|
+
@_memo_wise[index]&.delete(kwargs.first.last)
|
574
|
+
end
|
575
|
+
when MemoWise::InternalAPI::SPLAT
|
576
|
+
if args.empty?
|
577
|
+
@_memo_wise[index]&.clear
|
578
|
+
else
|
579
|
+
@_memo_wise[index]&.delete(args)
|
576
580
|
end
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
if api.use_hashed_key?(method_name)
|
581
|
-
@_memo_wise_hashes[method_name]&.delete(key)
|
582
|
-
@_memo_wise.delete(key)
|
581
|
+
when MemoWise::InternalAPI::DOUBLE_SPLAT
|
582
|
+
if kwargs.empty?
|
583
|
+
@_memo_wise[index]&.clear
|
583
584
|
else
|
584
|
-
@_memo_wise[
|
585
|
+
@_memo_wise[index]&.delete(kwargs)
|
586
|
+
end
|
587
|
+
else # MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
588
|
+
if args.empty? && kwargs.empty?
|
589
|
+
@_memo_wise[index]&.clear
|
590
|
+
else
|
591
|
+
key = if method_arguments == MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
592
|
+
[args, kwargs]
|
593
|
+
else
|
594
|
+
method.parameters.map.with_index do |(type, name), i|
|
595
|
+
type == :req ? args[i] : kwargs[name] # rubocop:disable Metrics/BlockNesting
|
596
|
+
end
|
597
|
+
end
|
598
|
+
@_memo_wise[index]&.delete(key)
|
585
599
|
end
|
586
600
|
end
|
587
601
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memo_wise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Panorama Education
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2021-
|
14
|
+
date: 2021-11-10 00:00:00.000000000 Z
|
15
15
|
dependencies: []
|
16
16
|
description:
|
17
17
|
email:
|
@@ -69,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
69
|
- !ruby/object:Gem::Version
|
70
70
|
version: '0'
|
71
71
|
requirements: []
|
72
|
-
rubygems_version: 3.2.
|
72
|
+
rubygems_version: 3.2.22
|
73
73
|
signing_key:
|
74
74
|
specification_version: 4
|
75
75
|
summary: The wise choice for Ruby memoization
|