Add rewrite rule to unfuse (text . drop) - #301
Conversation
|
No regression tests, no merge. EDIT: also, please put the explanations why the change is made into the commit messages. |
Are these requirements documented somewhere? Could they be added to a DEVELOPMENT.md or pull_request_template.md so that potential contributors can learn this up-front? |
Writing good commit messages and adding tests for your changes should be a common practice. But sure, we can be explicit. |
The combination of take . drop is commonly used as a substring operation. The original take and drop functions both work by calculating just a new offset and length. When fused, the streaming implementation unnecessarily copies the data. This makes it much slower and more memory consuming.
The combination of (take . drop) should not be fused. This test makes sure they are not by comparing the underlying array before and after applying those functions. See haskell#301.
da3b8cd to
f4dff92
Compare
|
The original commit has been rewritten to contain a more thorough explanation, and a regression test was added to make sure fusion doesn't happen. |
The combination of (take . drop) should not be fused. This test makes sure they are not by comparing the underlying array before and after applying those functions. See haskell#301.
f4dff92 to
8c0a8da
Compare
|
I wonder whether |
The combination of (take . drop) should not be fused. This test makes sure they are not by comparing the underlying array before and after applying those functions. See haskell#301.
Problem
While profiling some project, the following function turned out to be the largest bottleneck:
{-# INLINE slice #-} -- | Substring from @offset@ to @offset + len@ slice :: Int -> Int -> Text -> Text slice offset len = T.take len . T.drop offsetThe direct cause of this bottleneck is fusion. The original
takeanddropfunctions work by calculating a new offset and length. A new view ofTextcan be returned without copying the underlying array. However, theslicefunction is rewritten, and the streaming implementation unnecessarily copies the data. This makes it much slower and more memory consuming.With
takeanddropbeing fast individually, putting them together should not lead to a substantial decrease in performance.Proposed solution
Add a rewrite rule that specifically rewrites
take len . drop offsetback to an unfused version, just like theTEXT take -> unfusedandTEXT drop -> unfuseddo for their respective functions individually.I would argue that a rewrite rule for this very specific pair of functions is justified because it represents the very common substring operation. Since I don't think it would be wise to add
substringto the interface ofData.Text, optimizing its de facto implementation would be the next best thing.Benchmark
A project hosting a benchmark can be found at Channable/haskell-string-slicing-benchmarks. It benchmarks the original function, the rule added by this pull request and three other solutions. See
Bench.hs.Below are the results of the benchmark that showed the most significant difference between fused and unfused:
Benchmark results for above mentioned slice
Benchmark results when the rule of this PR is included
That's a huge difference, both in runtime and memory.
Open questions
last,tail,init,nullto name a few). Any pipeline involving only such functions might be better off not being fused. Could a different approach to fusion improve performance here?take n . drop mis very a common operation. Are there other common operations that would benefit from such rules?