memo_wise 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -2
- data/Gemfile.lock +1 -1
- data/README.md +29 -21
- data/benchmarks/benchmarks.rb +2 -4
- data/lib/memo_wise/internal_api.rb +59 -2
- data/lib/memo_wise/version.rb +1 -1
- data/lib/memo_wise.rb +24 -25
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ff51bfee4fcc634d50588005e79c4a74702fd0d8deb8ab23dd917134cedf42f
|
4
|
+
data.tar.gz: 1cb14df9bcef27a3442b48cf1aee370835bbf9070b1f44bb3d8ec8266158bdab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36a3f525833af24f076568ed9bdb43a06e90441788f2f8e8d26c0303ec7e0697a56c8401a3e24c06300cb3f2df199768ae00d8cf95cc90808fb4d9940ab9a931
|
7
|
+
data.tar.gz: f60569c63d693272856137c179b826a598c63a5762fedd76faf630b26fd3301d6ca37756f99cb7940501d5bcfd4c375094176a617610c35fc8788b67580159ce
|
data/CHANGELOG.md
CHANGED
@@ -5,9 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
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
|
-
## Unreleased
|
8
|
+
## [Unreleased]
|
9
|
+
|
9
10
|
- Nothing yet!
|
10
11
|
|
12
|
+
## [1.3.0] - 2021-11-22
|
13
|
+
|
14
|
+
- Fix thread-safety issue in concurrent calls to zero-arg method in unmemoized
|
15
|
+
state which resulted in a `nil` value being accidentally returned in one thread
|
16
|
+
- Fix bugs related to child classes inheriting from parent classes that use
|
17
|
+
`MemoWise`
|
18
|
+
|
11
19
|
## [1.2.0] - 2021-11-10
|
12
20
|
|
13
21
|
### Updated
|
@@ -93,7 +101,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
93
101
|
- Panolint
|
94
102
|
- Dependabot setup
|
95
103
|
|
96
|
-
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.
|
104
|
+
[Unreleased]: https://github.com/panorama-ed/memo_wise/compare/v1.3.0...HEAD
|
105
|
+
[1.3.0]: https://github.com/panorama-ed/memo_wise/compare/v1.2.0...v1.3.0
|
106
|
+
[1.2.0]: https://github.com/panorama-ed/memo_wise/compare/v1.1.0...v1.2.0
|
97
107
|
[1.1.0]: https://github.com/panorama-ed/memo_wise/compare/v1.0.0...v1.1.0
|
98
108
|
[1.0.0]: https://github.com/panorama-ed/memo_wise/compare/v0.4.0...v1.0.0
|
99
109
|
[0.4.0]: https://github.com/panorama-ed/memo_wise/compare/v0.3.0...v0.4.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -20,6 +20,7 @@
|
|
20
20
|
* Support for [resetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#reset_memo_wise-instance_method) and [presetting](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise#preset_memo_wise-instance_method) memoized values
|
21
21
|
* Support for memoization on frozen objects
|
22
22
|
* Support for memoization of class and module methods
|
23
|
+
* Support for inheritance of memoized class and instance methods
|
23
24
|
* Full [documentation](https://rubydoc.info/github/panorama-ed/memo_wise/MemoWise) and [test coverage](https://codecov.io/gh/panorama-ed/memo_wise)!
|
24
25
|
|
25
26
|
## Installation
|
@@ -56,9 +57,9 @@ class Example
|
|
56
57
|
x
|
57
58
|
end
|
58
59
|
memo_wise :slow_value
|
59
|
-
|
60
|
+
|
60
61
|
private
|
61
|
-
|
62
|
+
|
62
63
|
# maintains privacy of the memoized method
|
63
64
|
def private_slow_method(x)
|
64
65
|
sleep x
|
@@ -117,15 +118,15 @@ Results using Ruby 3.0.2:
|
|
117
118
|
|
118
119
|
|Method arguments|`Dry::Core`\* (0.7.1)|`Memery` (1.4.0)|
|
119
120
|
|--|--|--|
|
120
|
-
|`()` (none)|1.
|
121
|
-
|`(a)`|2.
|
122
|
-
|`(a, b)`|0.
|
123
|
-
|`(a:)`|2.
|
124
|
-
|`(a:, b:)`|0.
|
125
|
-
|`(a, b:)`|0.
|
126
|
-
|`(a, *args)`|0.
|
127
|
-
|`(a:, **kwargs)`|0.
|
128
|
-
|`(a, *args, b:, **kwargs)`|0.
|
121
|
+
|`()` (none)|1.42x|17.84x|
|
122
|
+
|`(a)`|2.48x|11.48x|
|
123
|
+
|`(a, b)`|0.46x|2.05x|
|
124
|
+
|`(a:)`|2.18x|20.50x|
|
125
|
+
|`(a:, b:)`|0.48x|4.22x|
|
126
|
+
|`(a, b:)`|0.48x|4.08x|
|
127
|
+
|`(a, *args)`|0.90x|1.97x|
|
128
|
+
|`(a:, **kwargs)`|0.80x|3.02x|
|
129
|
+
|`(a, *args, b:, **kwargs)`|0.61x|1.54x|
|
129
130
|
|
130
131
|
\* `Dry::Core`
|
131
132
|
[may cause incorrect behavior caused by hash collisions](https://github.com/dry-rb/dry-core/issues/63).
|
@@ -134,15 +135,15 @@ Results using Ruby 2.7.4 (because these gems raise errors in Ruby 3.x):
|
|
134
135
|
|
135
136
|
|Method arguments|`DDMemoize` (1.0.0)|`Memoist` (0.16.2)|`Memoized` (1.0.2)|`Memoizer` (1.0.3)|
|
136
137
|
|--|--|--|--|--|
|
137
|
-
|`()` (none)|
|
138
|
-
|`(a)`|
|
139
|
-
|`(a, b)`|3.
|
140
|
-
|`(a:)`|34.
|
141
|
-
|`(a:, b:)`|5.
|
142
|
-
|`(a, b:)`|4.
|
143
|
-
|`(a, *args)`|3.
|
144
|
-
|`(a:, **kwargs)`|2.
|
145
|
-
|`(a, *args, b:, **kwargs)`|2.
|
138
|
+
|`()` (none)|33.90x|3.44x|1.56x|4.03x|
|
139
|
+
|`(a)`|24.56x|17.02x|12.94x|14.91x|
|
140
|
+
|`(a, b)`|3.14x|2.35x|1.84x|2.03x|
|
141
|
+
|`(a:)`|34.42x|28.14x|23.83x|25.26x|
|
142
|
+
|`(a:, b:)`|5.13x|4.28x|3.77x|3.97x|
|
143
|
+
|`(a, b:)`|4.83x|4.08x|3.50x|3.66x|
|
144
|
+
|`(a, *args)`|3.03x|2.25x|1.95x|1.95x|
|
145
|
+
|`(a:, **kwargs)`|2.90x|2.44x|2.14x|2.24x|
|
146
|
+
|`(a, *args, b:, **kwargs)`|2.05x|1.77x|1.65x|1.64x|
|
146
147
|
|
147
148
|
You can run benchmarks yourself with:
|
148
149
|
|
@@ -206,7 +207,13 @@ after(:each) { helper.reset_memo_wise }
|
|
206
207
|
|
207
208
|
## Further Reading
|
208
209
|
|
209
|
-
We
|
210
|
+
We presented at RubyConf 2021:
|
211
|
+
|
212
|
+
- Achieving Fast Method Metaprogramming: Lessons from `MemoWise`
|
213
|
+
([slides](https://docs.google.com/presentation/d/1XgERQ0YHplwJKM3wNQwZn584d_9szYZp2WsDEXoY_7Y/edit?usp=sharing) /
|
214
|
+
[benchmarks](https://gist.github.com/JacobEvelyn/17b7b000e50151c30eaea928f1fcdc11))
|
215
|
+
|
216
|
+
And we've written more about `MemoWise` in a series of blog posts:
|
210
217
|
|
211
218
|
- [Introducing: MemoWise](https://medium.com/building-panorama-education/introducing-memowise-51a5f0523489)
|
212
219
|
- [Optimizing MemoWise Performance](https://ja.cob.land/optimizing-memowise-performance)
|
@@ -241,6 +248,7 @@ Then carry out these steps:
|
|
241
248
|
|
242
249
|
1. Update `CHANGELOG.md`:
|
243
250
|
- Add an entry for the upcoming version _x.y.z_
|
251
|
+
- Add a link for this version's comparison to the bottom of `CHANGELOG.md`
|
244
252
|
- Move content from _Unreleased_ to the upcoming version _x.y.z_
|
245
253
|
- Commit with title `Update CHANGELOG.md for x.y.z`
|
246
254
|
|
data/benchmarks/benchmarks.rb
CHANGED
@@ -63,8 +63,7 @@ BENCHMARK_GEMS = [
|
|
63
63
|
# Use metaprogramming to ensure that each class is created in exactly the
|
64
64
|
# the same way.
|
65
65
|
BENCHMARK_GEMS.each do |benchmark_gem|
|
66
|
-
# rubocop:disable Security/Eval
|
67
|
-
eval <<-CLASS, binding, __FILE__, __LINE__ + 1
|
66
|
+
eval <<~HEREDOC, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
|
68
67
|
# For these methods, we alternately return truthy and falsey values in
|
69
68
|
# order to benchmark memoization when the result of a method is falsey.
|
70
69
|
#
|
@@ -125,8 +124,7 @@ BENCHMARK_GEMS.each do |benchmark_gem|
|
|
125
124
|
end
|
126
125
|
#{benchmark_gem.memoization_method} :positional_splat_keyword_and_double_splat_args
|
127
126
|
end
|
128
|
-
|
129
|
-
# rubocop:enable Security/Eval
|
127
|
+
HEREDOC
|
130
128
|
end
|
131
129
|
|
132
130
|
# We pre-create argument lists for our memoized methods with arguments, so that
|
@@ -161,15 +161,72 @@ module MemoWise
|
|
161
161
|
def self.original_class_from_singleton(klass)
|
162
162
|
raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class?
|
163
163
|
|
164
|
+
# Since we call this method a lot, we memoize the results. This can have a
|
165
|
+
# huge impact; for example, in our test suite this drops our test times
|
166
|
+
# from over five minutes to just a few seconds.
|
167
|
+
@original_class_from_singleton ||= {}
|
168
|
+
|
164
169
|
# Search ObjectSpace
|
165
170
|
# * 1:1 relationship of singleton class to original class is documented
|
166
171
|
# * Performance concern: searches all Class objects
|
167
|
-
# But, only runs at load time
|
168
|
-
ObjectSpace.each_object(Module).find do |cls|
|
172
|
+
# But, only runs at load time and results are memoized
|
173
|
+
@original_class_from_singleton[klass] ||= ObjectSpace.each_object(Module).find do |cls|
|
169
174
|
cls.singleton_class == klass
|
170
175
|
end
|
171
176
|
end
|
172
177
|
|
178
|
+
# Increment the class's method index counter, and return an index to use for
|
179
|
+
# the given method name.
|
180
|
+
#
|
181
|
+
# @param klass [Class]
|
182
|
+
# Original class on which a method is being memoized
|
183
|
+
#
|
184
|
+
# @param method_name [Symbol]
|
185
|
+
# The name of the method being memoized
|
186
|
+
#
|
187
|
+
# @return [Integer]
|
188
|
+
# The index within `@_memo_wise` to store the method's memoized results
|
189
|
+
def self.next_index!(klass, method_name)
|
190
|
+
# `@_memo_wise_indices` stores the `@_memo_wise` indices of different
|
191
|
+
# method names. We only use this data structure when resetting or
|
192
|
+
# presetting memoization. It looks like:
|
193
|
+
# {
|
194
|
+
# single_arg_method_name: 0,
|
195
|
+
# other_single_arg_method_name: 1
|
196
|
+
# }
|
197
|
+
memo_wise_indices = klass.instance_variable_get(:@_memo_wise_indices)
|
198
|
+
memo_wise_indices ||= klass.instance_variable_set(:@_memo_wise_indices, {})
|
199
|
+
|
200
|
+
# When a parent and child class both use `class << self` to define
|
201
|
+
# memoized class methods, the child class' singleton is not considered a
|
202
|
+
# descendent of the parent class' singleton. Because we store the index
|
203
|
+
# counter as a class variable that can be shared up the inheritance chain,
|
204
|
+
# we want to detect this case and store it on the original class instead
|
205
|
+
# of the singleton to make the counter shared correctly.
|
206
|
+
counter_class = klass.singleton_class? ? original_class_from_singleton(klass) : klass
|
207
|
+
|
208
|
+
# We use a class variable for tracking the index to make this work with
|
209
|
+
# inheritance structures. When a parent and child class both use
|
210
|
+
# MemoWise, we want the child class's index to not "reset" back to 0 and
|
211
|
+
# overwrite the behavior of a memoized parent method. Using a class
|
212
|
+
# variable will share the index data between parent and child classes.
|
213
|
+
#
|
214
|
+
# However, we don't use a class variable for `@_memo_wise_indices`
|
215
|
+
# because we want to allow instance and class methods with the same name
|
216
|
+
# to both be memoized, and using a class variable would share that index
|
217
|
+
# data between them.
|
218
|
+
index = if counter_class.class_variable_defined?(:@@_memo_wise_index_counter)
|
219
|
+
counter_class.class_variable_get(:@@_memo_wise_index_counter)
|
220
|
+
else
|
221
|
+
0
|
222
|
+
end
|
223
|
+
|
224
|
+
memo_wise_indices[method_name] = index
|
225
|
+
counter_class.class_variable_set(:@@_memo_wise_index_counter, index + 1) # rubocop:disable Style/ClassVars
|
226
|
+
|
227
|
+
index
|
228
|
+
end
|
229
|
+
|
173
230
|
# Convention we use for renaming the original method when we replace with
|
174
231
|
# the memoized version in {MemoWise.memo_wise}.
|
175
232
|
#
|
data/lib/memo_wise/version.rb
CHANGED
data/lib/memo_wise.rb
CHANGED
@@ -56,7 +56,7 @@ module MemoWise
|
|
56
56
|
# :nocov:
|
57
57
|
all_args = RUBY_VERSION < "2.7" ? "*" : "..."
|
58
58
|
# :nocov:
|
59
|
-
class_eval
|
59
|
+
class_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
60
60
|
# On Ruby 2.7 or greater:
|
61
61
|
#
|
62
62
|
# def initialize(...)
|
@@ -75,7 +75,7 @@ module MemoWise
|
|
75
75
|
MemoWise::InternalAPI.create_memo_wise_state!(self)
|
76
76
|
super
|
77
77
|
end
|
78
|
-
|
78
|
+
HEREDOC
|
79
79
|
|
80
80
|
# @private
|
81
81
|
#
|
@@ -156,6 +156,17 @@ module MemoWise
|
|
156
156
|
klass = klass.singleton_class
|
157
157
|
end
|
158
158
|
|
159
|
+
if klass.singleton_class?
|
160
|
+
# This ensures that a memoized method defined on a parent class can
|
161
|
+
# still be used in a child class.
|
162
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
163
|
+
def inherited(subclass)
|
164
|
+
super
|
165
|
+
MemoWise::InternalAPI.create_memo_wise_state!(subclass)
|
166
|
+
end
|
167
|
+
HEREDOC
|
168
|
+
end
|
169
|
+
|
159
170
|
raise ArgumentError, "#{method_name.inspect} must be a Symbol" unless method_name.is_a?(Symbol)
|
160
171
|
|
161
172
|
api = MemoWise::InternalAPI.new(klass)
|
@@ -167,39 +178,27 @@ module MemoWise
|
|
167
178
|
klass.send(:private, original_memo_wised_name)
|
168
179
|
|
169
180
|
method_arguments = MemoWise::InternalAPI.method_arguments(method)
|
170
|
-
|
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)
|
181
|
+
index = MemoWise::InternalAPI.next_index!(klass, method_name)
|
183
182
|
|
184
183
|
case method_arguments
|
185
184
|
when MemoWise::InternalAPI::NONE
|
186
185
|
# Zero-arg methods can use simpler/more performant logic because the
|
187
186
|
# hash key is just the method name.
|
188
|
-
klass.module_eval
|
187
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
189
188
|
def #{method_name}
|
190
|
-
|
191
|
-
|
192
|
-
_memo_wise_output
|
189
|
+
if @_memo_wise_sentinels[#{index}]
|
190
|
+
@_memo_wise[#{index}]
|
193
191
|
else
|
192
|
+
ret = @_memo_wise[#{index}] = #{original_memo_wised_name}
|
194
193
|
@_memo_wise_sentinels[#{index}] = true
|
195
|
-
|
194
|
+
ret
|
196
195
|
end
|
197
196
|
end
|
198
|
-
|
197
|
+
HEREDOC
|
199
198
|
when MemoWise::InternalAPI::ONE_REQUIRED_POSITIONAL, MemoWise::InternalAPI::ONE_REQUIRED_KEYWORD
|
200
199
|
key = method.parameters.first.last
|
201
200
|
|
202
|
-
klass.module_eval
|
201
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
203
202
|
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
204
203
|
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
205
204
|
_memo_wise_output = _memo_wise_hash[#{key}]
|
@@ -209,7 +208,7 @@ module MemoWise
|
|
209
208
|
_memo_wise_hash[#{key}] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
210
209
|
end
|
211
210
|
end
|
212
|
-
|
211
|
+
HEREDOC
|
213
212
|
# MemoWise::InternalAPI::MULTIPLE_REQUIRED, MemoWise::InternalAPI::SPLAT,
|
214
213
|
# MemoWise::InternalAPI::DOUBLE_SPLAT, MemoWise::InternalAPI::SPLAT_AND_DOUBLE_SPLAT
|
215
214
|
else
|
@@ -225,7 +224,7 @@ module MemoWise
|
|
225
224
|
# consistent performance. In general, this should still be faster for
|
226
225
|
# truthy results because `Hash#[]` generally performs hash lookups
|
227
226
|
# faster than `Hash#fetch`.
|
228
|
-
klass.module_eval
|
227
|
+
klass.module_eval <<~HEREDOC, __FILE__, __LINE__ + 1
|
229
228
|
def #{method_name}(#{MemoWise::InternalAPI.args_str(method)})
|
230
229
|
_memo_wise_hash = (@_memo_wise[#{index}] ||= {})
|
231
230
|
_memo_wise_key = #{MemoWise::InternalAPI.key_str(method)}
|
@@ -236,7 +235,7 @@ module MemoWise
|
|
236
235
|
_memo_wise_hash[_memo_wise_key] = #{original_memo_wised_name}(#{MemoWise::InternalAPI.call_str(method)})
|
237
236
|
end
|
238
237
|
end
|
239
|
-
|
238
|
+
HEREDOC
|
240
239
|
end
|
241
240
|
|
242
241
|
klass.send(visibility, method_name)
|
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.3.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-11-
|
14
|
+
date: 2021-11-23 00:00:00.000000000 Z
|
15
15
|
dependencies: []
|
16
16
|
description:
|
17
17
|
email:
|