3 ms·
Not the OP, but one I think about a lot is converting functional, monadic code that uses first class functions/closures to equivalent imperative variants. For
by johncolanduoni 11y ago
Not the OP, but one I think about a lot is converting functional, monadic code that uses first class functions/closures to equivalent imperative variants.
For example, manipulating collections with functions like map, flatMap, filter, etc. has become common, even in popular imperative languages like C# and Java. These calls are can be chained together to create non-strict sequences (IEnumerable, Stream, etc.) which are made strict at a later time. Each call creates a new sequence, and many take closures. Both of these require memory allocation and indirect dispatch.
However, in many cases it extremely straightforward to convert these into loops. For example, the following C# code:
list.Select(f).Where(p).SelectMany(g).ToList()
Could be turned into:
var outList = new List<T>();
foreach(var x in list) {
var y = f(x);
if(!p(y))
continue;
foreach(var z in g(y)) {
outList.Add(z)
}
}
This works, even if we known nothing about f, p, and g (e.g. they can be impure functions). This optimization is especially effective if these functions are lambdas. It is also always going to be faster and safe; the only difference is that we removed a series of extra allocations and indirect function calls. This example is admittedly simple, but many functional patterns and behaviors can be rewritten into longer imperative variants that avoid extra allocations.
You are right that you can have subtle changes in behavior with similar high level optimizations. For example, most functional languages make calls like these actually create a new collection at each call. If any of these functions can throw and exception or cause an effect, then we cannot perform the above rewriting because if will call the functions out of order. But if a compiler can take the time to analyze functions for external purity, these can be optimized as well.
- kibwen 11y agoYour example doesn't necessarily bolster this thread's original point, because compiler backends are already sophisticated enough to apply this sort of transformation regularly (e.g. you'll see this sort of code generated in Rust, where closures are stack-allocated, iterators are lazy and single-pass, and the resulting loops can even be vectorized).
- johncolanduoni 11y agoYou're absolutely right, I chose a simple example because it's one I expect most people have seen in one language or another. A better one would be rewriting a combinator based parser (e.g. one written in Parsec) into an imperative one.
- lunchTime42 11y agoDont you cave in blindly believing into the infoscience gods of old having done good. Demand profile logs, demand numbers, demand evidence. Compilers do the most utterly unexpected nonsense, forget the most trivial optimisations. Take your code to https://gcc.godbolt.org/ https://gcc.godbolt.org/ and read what your "Compiler-knows-best" does with your code. yes, it doesent matter often in the end, cause even bad code must wait for worser memory, but still..
- joosters 11y agoCould you give an example of 'forgetting the most trivial optimizations' please? It would be a bug in a compiler and many people would be interested in fixing it.