-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Get rid of eval #59
base: master
Are you sure you want to change the base?
Get rid of eval #59
Conversation
Fix #48, at the cost of putting the variable in a poorly-performing global. Not sure if this is acceptable. It's frustrating that Julia seemingly lacks the tools to deal with this elegantly. - If `const` worked in a local function, we'd just put `const` and be done with it. - If `typeconst` existed for global variables, that would work too. Memoization.jl uses generated functions, which causes other problems. And it feels like the wrong solution too.
Maybe begin
function foo end
let memo=...
foo(x) = get(memo, x) do ... end
end
end EDIT: Nope, doesn't work at global scope. |
Ref discussion in https://discourse.julialang.org/t/how-to-write-memoize-that-works-in-both-local-and-global-scope/49727/6 I'm increasingly of the opinion that if we ever do a 2.0 release, we should have a separate |
I implemented the solution in #48 (comment). It's very clean and elegant, and it allows us to get rid of EDIT: figured it out. Now there's only one |
I fixed the last remaining test with try
# So that redefining a function doesn't leak memory through the previous cache.
empty!(memoize_cache($f))
catch
end but that brings back the |
This problem is going over my head a bit. What can I look at, specifically? I did notice that the clean cache tests are not in this branch; are you doing those manually? |
Maybe you looked before I rebased on top of master. The cache tests should be here now, and I did find a simple solution to the cache-wiping problem. Now everything works, but I have to admit that with all the bugfixes, this PR has lost a lot of its appeal. |
Hmm, life of a software engineer. |
For correctness, I don't think we need to empty the cache if we simply formalize the expectation that the cache passed to memoize only contains correct values (this would be easier to state if the syntax was to simply supply an expression which evaluated to an AbstractDict rather than a no-argument function that returned a dictionary. But I digress...). Note that an empty dictionary only contains correct values. If a method is overwritten, the new method will have a new cache of correct values. If the new method is just a specialization of a previous, then the cache of the old method will only get called with arguments that correspond to the still correct elements of the old cache. While I understand why we may want to invalidate caches for garbage collection, I'd like to post an example where this seems too eager.
In this example, we end up recalculating the old fib, even though the values in the cache of Furthermore, while emptying the old cache gets rid of most of its memory impact, the old dictionary itself is never erased. The old dictionary itself is still there, referenced by the What if instead of emptying the cache every time, we change |
Reflecting a bit, I realize now that my previous comment was written under the assumption that only the memoized function gets redefined, and that it doesn't reference any changing global state. I suspect it would be tricky (and likely out of scope) to invalidate the cache whenever functions called by the memoized function are redefined, or when relevant global state is changed. |
Since this is a per-method cache, it is breaking. It's also incorrect when clearing the cache of a function with multiple cached methods. |
Yeah, this is the bug I describe just above.
If clearing up the cache is about freeing memory after a Revision, then leaking a few empty Dicts' memory is inconsequential...
Returning a list of all dictionaries would fix to the bug described. I don't know if we should stop emptying the cache on redefinition. Before this PR, emptying the Dict was more critical, because otherwise bad results could be returned. With this PR, it's "just" a memory concern during interactive development. Come to think of it, is there an argument against emptying the (now unreachable) Dict on method redefinition? It makes the code a bit uglier, but that's it, no?
Yes, that's all true. Cache invalidation is complex. I use |
It makes the macro expansion much more palatable
Another point in favor of this PR: it's not particularly hard to share the cache between methods if that's the desired behaviour, with cache = Dict()
@memoize cache foo(x::Int) = ...
@memoize cache foo(x::Vector) = ... |
Here are three reasons not to empty the old cache.
In this PR, the second invocation of |
Right, but that would be fixed by the list-of-caches approach. We should only wipe the redefined method's cache.
Only if you redefine the function. At some point, it seems reasonable to carve some use case domain for Memoize.jl. Perhaps we can suggest in the README to use cache = OnDiskDict("blah.data")
foo(x) = get!(cache, x) do
...
end
Right, it should be fixed before merging. |
Ah, I see. So instead of a list of caches, you're proposing a dictionary of caches mapping each method to its cache. Then, when a method is redefined (in this case, Julia gives a warning regarding method overwriting I think), you would empty only the cache corresponding to the overwritten method to clear up memory. In that case, I agree with you that it's pretty safe to empty the cache. Storing the method associated with each cache has the added bonus of affording the user more granularity when clearing caches manually. We could define functions like You're right that this solution would work with #68 assuming the user doesn't overwrite methods, so I'm in favor. As an aside: It's too bad the gc doesn't see that there's no way to access the overwritten cache and take care of deleting it on its own. |
To recap this PR, because I don't see myself working on it anytime soon:
|
Fix #48, at the cost of putting the cache in a poorly-performing global.
Not sure if this is acceptable. It's frustrating that Julia seemingly lacks the tools to deal with this elegantly.
const
worked in a local function, we'd just putconst
and be done with it.typeconst
existed for global variables, that would work too.Memoization.jl uses generated functions, which causes other problems. And it feels like the wrong solution too.
Maybe we should put the
const
, and have a separate@memoize_local
for local functions? Feels like an acceptable trade-off.