-
Notifications
You must be signed in to change notification settings - Fork 178
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
Optimize IntMap.alter using unboxed sums. #523
base: master
Are you sure you want to change the base?
Conversation
We use unboxed sums (available in GHC >= 8.2) to track whether or not the map required modification. Benchmark results: ------------------ Before: ------- benchmarking alter time 881.1 μs (862.0 μs .. 899.2 μs) 0.996 R² (0.994 R² .. 0.998 R²) mean 854.9 μs (841.8 μs .. 872.5 μs) std dev 49.55 μs (38.01 μs .. 69.97 μs) variance introduced by outliers: 48% (moderately inflated) After: ------ benchmarking alter time 513.1 μs (506.5 μs .. 519.6 μs) 0.998 R² (0.997 R² .. 0.999 R²) mean 517.8 μs (511.7 μs .. 528.5 μs) std dev 27.05 μs (17.74 μs .. 47.52 μs) variance introduced by outliers: 45% (moderately inflated)
I'll add more fine grained benchmarks if I get the chance tomorrow. Sending out for feedback on the code. |
-- | ||
-- If no modifications are made to the map (# (# #) | #) is returned, otherwise | ||
-- (# | newMap #) is returned. | ||
alter# :: (Maybe a -> Maybe a) -> Key -> IntMap a -> (# (# #) | IntMap a #) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you make the function have type Maybe# a -> Maybe# a
, where Maybe#
is the unboxed version of Maybe
? Couldn't we often avoid the Maybe
allocation that way? Benchmark!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That function is user defined (passed to alter
) so at some point you would have to do the conversion from Maybe
to Maybe#
. As far as I know coerce wouldn't work for f
because they have different representations (Maybe
and Maybe#
that is), but there may be another way that I'm unaware of. Did you have a specific approach in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a decent bet that the function we're passed will be small enough to inline. So calling alter#
with
\ p -> fromMaybe (f (toMaybe p))
(essentially) should usually avoid any actual Maybe
s. Or so I imagine.
So, as is always the case, using unboxed sums makes some operations faster, and others a lot slower :( Before:
After:
It looks like in cases where we need to modify the spine anyways the extra pattern matches (despite being unboxed sums) on every |
alter# f !k t@(Bin p m l r) | ||
| nomatch k p m = case f Nothing of | ||
Nothing -> (# (# #) | #) | ||
Just x -> (# | link k (Tip k x) p t #) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you're being strict enough with your return values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean something like the following?
let !t' = link k (Tip k x) p t in (# | t' #)
Or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but see the link below for a clearer way.
-- | ||
-- If no modifications are made to the map (# (# #) | #) is returned, otherwise | ||
-- (# | newMap #) is returned. | ||
alter# :: (Maybe a -> Maybe a) -> Key -> IntMap a -> (# (# #) | IntMap a #) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a decent bet that the function we're passed will be small enough to inline. So calling alter#
with
\ p -> fromMaybe (f (toMaybe p))
(essentially) should usually avoid any actual Maybe
s. Or so I imagine.
Take a look at treeowl@6f6fa02. That's not very well organized yet, and doesn't deal with the lazy version, but it shows how the strict version can be made more eager, how pattern synonyms can cut down dramatically on the noise, and how we can "unbox" the |
We need to benchmark the unboxed sums version against one that just uses pointer equality on each recursive call. |
Thanks for the info David! I've been realllllly busy the last week, sorry I haven't had time to move this forward as quickly as I'd like. I'm hoping to get around to this sometime this week. |
Oh geeze it's been a while since I've looked at this, and realistically I won't have substantial time to look into this and incorporate the suggestion's from the comment above in the near future. |
I will try to look at it shortly. No guarantees. |
I've marked this PR as a draft to clarify that it's not ready for final review / merge. |
Shall I try to take this up and finish it - anyone know what there is to do left? |
I don't remember,, sorry. Lost track of this one. Worth trying! |
That would be great! :)
I think the minimum is to fix any strictness issues such as #523 (comment). If the benchmarks show good improvements at this stage, I think this should be good to merge. Adopting some of noise reduction measures suggested in treeowl@6f6fa02 probably wouldn't hurt though. Regarding further optimization ideas, I think those could be left for further work. |
What is the status on this one? Are these benchmarks numbers still the same on GHC 9.2? |
Has something changed there? |
One can only hope, that's why I'm asking :) |
I haven't tested recently. Do you have time to try? |
No unfortunately I'm quite committed on Haddock and other stuff for the time being. How's the maintainer structure for |
Use
ptrEq
and unboxed sums (available in GHC >= 8.2) to track whether or not the maprequired modification. If not skip rebuilding a spine and return the original map.
Benchmark results:
Before:
After: (41% speedup)