You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After pattern-matching on lazy values, and then using the binded name, they are computed again.
f:LazyBool->Bool
f x@True= x
f x@False= x
g:LazyBool->Bool
g (Delay x@True) = x
g (Delay x@False) = x
h:LazyBool->Bool
h x@(DelayTrue) = x
h x@(DelayFalse) = x
main:IO()
main =do
ignore $ pure $ f $ trace "f"True
ignore $ pure $ g $ trace "g"True
ignore $ pure $ h $ trace "h"True{- f f g h h-}
g computes the lazy value only once, and h computes the lazy value twice. But in practice we usually write as in f, which behaves like h, which leads to performance issue.
A similar problem occurs when using case:
f:LazyBool->Bool
f x =case x ofTrue=> x
False=> x
g:LazyBool->Bool
g (Delay x) =case x ofTrue=> x
False=> x
main:IO()
main =do
ignore $ pure $ f $ trace "f"True
ignore $ pure $ g $ trace "g"True{- f f g-}
Problem with filter
This becomes a major problem in polymorphic functions when we have no way to explicitly force the evaluation.
An example of such a function is filter. It behaves similarly to the functions above: it evaluates a value, and in a result uses the name binded with the original argument. Therefore, it is inefficient to filter lazy values. Instead of filter (p . force) it is more efficient to do map delay . filter p . map force. Here are measurements demonstrating this problem:
iterate:Nat-> (a -> a) -> a -> a
iterate0 f =iditerate (S n) f =iterate n f . f
true: a ->Bool
true _=TruerunWithTimer: String ->IO a ->IO()
runWithTimer title f =do
start <- clockTime UTC
ignore f
end <- clockTime UTCputStrLn$"\{title}: \{showTime 0 2 $ timeDifference end start}"filterLazy:Functor f=> ((a ->Bool) -> f a -> f a) -> (a ->Bool) -> f (Lazy a) -> f (Lazy a)
filterLazy filt p =map delay . filt p .map force
main:IO()
main =dolet iterations =100
complexity =10000000value:Lazy_ = [0..complexity]
runWithTimer "Fast Maybe"$ pure $iterate iterations (filterLazy filter true) $ pure {f=Maybe} value
runWithTimer "Slow Maybe"$ pure $iterate iterations (filter$ true . force) $ pure {f=Maybe} value
runWithTimer "Fast List"$ pure $iterate iterations (filterLazy filter true) $ pure {f=List} value
runWithTimer "Slow List"$ pure $iterate iterations (filter$ true . force) $ pure {f=List} value
runWithTimer "Fast SnocList"$ pure $iterate iterations (filterLazy filter true) $ pure {f=SnocList} value
runWithTimer "Slow SnocList"$ pure $iterate iterations (filter$ true . force) $ pure {f=SnocList} value
runWithTimer "Fast SnocListTR"$ pure $iterate iterations (filterLazy filterTR true) $ pure {f=SnocList} value
runWithTimer "Slow SnocListTR"$ pure $iterate iterations (filterTR $ true . force) $ pure {f=SnocList} value
Lines with filter compute the lazy value iterations times, while lines with filterLazy only 1 time:
Fast Maybe: 0.83s
Slow Maybe: 80.14s
Fast List: 0.94s
Slow List: 79.54s
Fast SnocList: 0.97s
Slow SnocList: 79.86s
Fast SnocListTR: 0.96s
Slow SnocListTR: 86.21s
I think it's currently impossible to write a polymorphic filter that handles lazy values efficiently.
Expected Behavior
Lazy values should not be evaluates again where the result is already known
f should be equivalent to g (maybe h too?).
filter (p . force) should behave like map delay . filter p . map force
Observed Behavior
Multiple evaluations of lazy values
g is more effective than f
map delay . filter p . map force is more effective than filter (p . force)
The text was updated successfully, but these errors were encountered:
spcfox
changed the title
Multiple calculation of lazy values
Multiple evaluations of lazy values
Nov 22, 2024
Have you tried with the --directive lazy=weakMemo compilation directive? (documented here) PR #2791 added weak memoization of lazy values, but it looks like it's guarded by that compilation directive because it caused performance regression in some cases.
Yes, memoization is a good way to combat this problem (although given that memoization is weak, I'm not sure the problem wouldn't show up in more complex examples). But I think it's an undesirable behavior, and it would be nice to fix it. I think the filter example is pretty illustrative.
Gist with code
Steps to Reproduce
After pattern-matching on lazy values, and then using the binded name, they are computed again.
g
computes the lazy value only once, andh
computes the lazy value twice. But in practice we usually write as inf
, which behaves likeh
, which leads to performance issue.A similar problem occurs when using
case
:Problem with
filter
This becomes a major problem in polymorphic functions when we have no way to explicitly force the evaluation.
An example of such a function is
filter
. It behaves similarly to the functions above: it evaluates a value, and in a result uses the name binded with the original argument. Therefore, it is inefficient to filter lazy values. Instead offilter (p . force)
it is more efficient to domap delay . filter p . map force
. Here are measurements demonstrating this problem:Lines with
filter
compute the lazy valueiterations
times, while lines withfilterLazy
only 1 time:I think it's currently impossible to write a polymorphic
filter
that handles lazy values efficiently.Expected Behavior
f
should be equivalent tog
(maybeh
too?).filter (p . force)
should behave likemap delay . filter p . map force
Observed Behavior
g
is more effective thanf
map delay . filter p . map force
is more effective thanfilter (p . force)
The text was updated successfully, but these errors were encountered: