-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
proposal: spec: memoization #62637
Comments
Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes |
Thanks, I wasn't sure if it was needed for this proposal or not. It's now updated. |
It seems to me that we could write a general purpose memoization package instead, and a function could use that. I don't see a reason that this should be built into the language. |
would intern #62483 be such a package? |
I don't quite understand. Can you write out all the APIs of the package to help understand? |
Is it possible to use |
@psnilesh as I understand it I'd think no because some functions are probably only executed at runtime. Wrt the proposal I wonder if it doesn't require to tie into the runtime. If done at a library level, it's basically a value cache that function writers could import and would retrieve value from if it exists. Probably the better and easier way to do this. I'd join @ianlancetaylor on that. Just a question in passing, although I don't think that these are the kinds of language changes that are usually considered for go, but |
I agree with Ian that this feels more appropriate to be a library feature than a language feature. Having memoization built into a language is not a totally unreasonable idea, but this level of abstraction feels out of step with the rest of Go, and suggests a built-in feature for a need that is -- based on my experience, at least -- a relatively specialized one that isn't important for most programs. (Of course that's just a hunch; I don't have data to support it.) When I think about API for this I think of something like // ManyOnce is a terrible name. I'm more focused on the API than the
// name for the moment.
type ManyOnce[ReqKey comparable, Result any] struct {
// ...
}
func (mo *ManyOnce[ReqK, R]) Do(k RK, f func () R) R {
// ...
} The The same caveats apply as for So from a callsite it might appear like this (based on the motivating example in the proposal): var results ManyOnce[string, int]
func heavyComputation(input string) int {
return results.Do(input, func () int {
return // code that takes a long time to process
})
} From the perspective of Does that seem like it would meet the original stated need, or have I missed something? |
@apparentlymart now that you mention it, isn't it possible to simply use sync.Once in a closure? (edit: answering to self, it's not)
Edit: since memoization is per function and functions are not comparable, one should probably use a function pointer as key in the global result map. Naively: But now making that automatic and generic is probably another ballgame. |
@atdiar unfortunately, while In general I see that it's a controversial proposal, but at least I shared my thoughts. It's certainly feasible to achieve a form of memoization using a library. I've personally implemented it here. Nevertheless, it's worth noting that Go still lacks certain essential features for this solution to be considered flawless, such as variadic generics for function arguments. |
It is true that the API I proposed would suffer from the typical problem that it isn't possible to express multiple return values from a function as a single type. But I would suggest then that it would be better to fix that wart in the language design, which would be an orthogonal change that would solve for many variations of this problem, than to introduce a "memoization" feature to work around that design problem in only one situation. (There are already various existing proposals open for variations on how we might deal with that schism, so I don't think we need to discuss the details of that here, only to consider what we might do if any one were approved and implemented.) |
@quantumsheep yes, it is not easy. The problems I see are:
I think you're right that it will be difficult to do that in a library without answers to at least the above. |
For the function id I use Knowing that functions are pure seems impossible with today's specs and I don't this a feature like this will be implemented. Every functions implemented by now would need to be marked pure. However the compiler could infer pure functions under certain conditions, it's clearly feasible but I'm not sure about the cost/benefit ratio. We could have variadic generic arguments: func example[T any, Args ...any](f func(args ...Args) T) func(args ...Args) T {
return func(args ...Args) T {
return f(args...)
}
} What do you think ? There might already be an existing proposal for this thought. |
Yes you could use that or use fmt.Sprint(f) where f is the function. Wrt purity, unfortunately, I'd tend to think that it is necessary. Memoizing a function that can have side-effects seems a bit too dangerous. Wrt variadic generic arguments, I admit I am not too sure I understand exactly variadic type parameters. I think the real feature would be function constraints that are arity-independent. And even then, the theory deems it impossible as it would be equivalent to having types of infinite size. So to conclude, I'm afraid it will be impossible. [edit] it might be possible in fact to envision variadic type parameters as a way to construct an object that is similar to a struct type and whose components are each of given type, but it will also require to have the equivalence between function argument lists and this type. Essentially some kind of tuple type. This is involved but probably necessary to have that kind of equivalence because function returns have to be assignable to variables and it's not possible/desirable to have variadic variable declarations as well. But that's just merely an idea. Another question: for a function to be memoizable, shouldn't the type of each function argument comparable? |
Based on the discussion above, and the emoji voting, this is a likely decline. Leaving open for three weeks for final comments. |
I am inclined to support the proposal. First, if you write a general memorization package, suppose a function has three arguments, two return values, and five different value types, the implementation method I think of is to use reflection. But the reflection performance is not high and the readability is poor. Or the memo from the author of the proposal, which I tested and found that the memorized function (which is just a recursive solution to the Fipolacci sequence) has 3 allocs/op, and the original function is 0 allocs/op. If it is a more complex function, I am not sure that there will not be more malloc. Second, I think using this proposal for a function that has side effects Third, I think the question of whether a memorized function can be called from multiple Goroutines at the same time is more open to discussion. |
No substantial change in consensus. |
As the @apparentlymart pointed out. it's possible to do a generic solution. but the function interface still has some constraints like the number of arguments. For me I have wrote my own solution to memoize functions of type |
Author background
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Experienced (working professionally with Go).
What other languages do you have experience with?
Rust, TypeScript, Python, C, C++, C#.
Related proposals
Has this idea, or one like it, been proposed before?
It does not seem so, at least I haven't found one similar case searching the current issues.
Does this affect error handling?
No.
Is this about generics?
No.
Proposal
What is the proposed change?
A build-in memoization system.
Who does this proposal help, and why?
This feature would help developers relying on manual memoization drastically reduce code boilerplate and complexity. Especially when dealing with a lots of functions that requires memoization.
Please describe as precisely as possible the change to the language.
Having a cache system to optimize compute-heavy functions is not an uncommon practice. Here's an example:
While this code is fine, maintaining multiple functions with similar caching logic, especially when dealing with functions that have multiple or variadic arguments, can become difficult.
I propose to introduce a built-in
memo
keyword prefixing a function definition.We would need to think about how to handle complex data structures and pointers (should we compare the pointers or the content of the pointers ?).
This concept can be thought of as a syntactic sugar feature, which may not align perfectly with Go's typical design principles. However, I'm still sharing the idea anyway to see what the community thinks.
What would change in the language spec?
The addition of a new keyword along with function "options" will be required.
Please also describe the change informally, as in a class teaching Go.
Sometimes in Go we want to speed up a function that takes a lot of time to process but with the same inputs. A good alternative is memoization where you cache the output of a function for the given inputs. When the function is called another time with the same inputs, the cached output is fetched and returned directly without processing the instructions again.
This proposal would remove any boilerplate needed to perform this task in favor of a single keyword.
Is this change backward compatible?
Yes, it would not change how previous Go code worked.
Orthogonality: how does this change interact or overlap with existing features?
Is the goal of this change a performance improvement?
Memoization is optimization so yes.
If so, what quantifiable improvement should we expect?
How would we measure it?
Costs
Would this change make Go easier or harder to learn, and why?
This proposal requires developers to know about memoization and it's a new keyword so I suppose it would make it harder to learn Go entirely.
What is the cost of this proposal? (Every language change has a cost).
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
I'm not familiar on how tools like these handles Go code but current parsers will certainly fail if this feature is added.
What is the compile time cost?
Should be negligible but I'm not sure as I haven't dig into the Go compiler.
What is the run time cost?
It will certainly add a little overhead to each functions that have this option enabled as it requires to check the presence of a value in a data structure.
Can you describe a possible implementation?
I never got my hands into the Go compiler so I'm not sure. However I think we would need a map for each function, each map having the function arguments as key and output as value.
We could have a TTL by cleaning the old cache with the GC so it does not impact the main process' performances.
The text was updated successfully, but these errors were encountered: