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