memo_wise 1.2.0 → 1.3.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/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:
|