Skip to content

Make 'length' fusible - #307

Merged
Shimuuar merged 1 commit into
haskell:masterfrom
gksato:fusible-length
Jun 21, 2020
Merged

Shimuuar merged 1 commit into
haskell:masterfrom
gksato:fusible-length

Conversation

@gksato

@gksato gksato commented Jun 3, 2020 •

Copy link
Copy Markdown
Contributor

This change resolves #306: it makes fusible Data.Vector.Generic.length, which currently can be inlined but doesn't fuse well. Before this revision, length uses stream', which is non-fusible.
This revision replaces stream' with stream, being fusible, and at the same time substitute the mutually-recursive occurrences of length with basicLength.

Rationale:
The current definition was introduced in the commit a811a86.
Prior to that commit, length could not even be inlined, because GHC elected it to be a loop-breaker due to the cyclic references among stream, clone, length, and unsafeCopy. This failure of inlining was reported as #97.

The commit a811a86 resolved this by replacing stream with stream' in the definition of length. The function stream refers to clone in its fusion rule, but stream' doesn't possess any rule. This cuts open the cycle of references and makes length inlinable.

However, since stream' doesn't possess any rule, defining

length = Bundle.length . stream'

is all the same as setting

length = basicLength

This prevents any stream fusion from happening.

This pull request resolves this problem by resetting length back to be Bundle.length . stream, and instead replacing the occurrences of length making cycles with basicLength (just inlined the simplification result of the definition). Now length is both inlinable and fusible.

The comparison test of the compilation of a simple program is in https://gist.github.com/gksato/417030a1f19f1f376dcb2fabaf66c10a .

Fixes: #306
See also: #97 #111 #155

@cartazio

cartazio commented Jun 3, 2020

Copy link
Copy Markdown
Contributor

Sounds very awesome! I’m going to make sure I look at this soon , though just in case I miss anything I’ll ask a few other folks to have a look too! (Lots of chaos in nyc etc this week )

Cc @sjakobi @Bodigrim @emilypi

@gksato

gksato commented Jun 3, 2020

Copy link
Copy Markdown
Contributor Author

Alright! Stay safe.

@gksato

gksato commented Jun 3, 2020

Copy link
Copy Markdown
Contributor Author

@Shimuuar

Copy link
Copy Markdown
Contributor

I finally go to review this PR. I digged through code and now I mildly confused. Does original loop stream, clone, length, unsafeCopy still exists? It seems stream was rewritten in meantime to use Bundle.

@gksato did you try to just use stream for length definition?

P.S. this just illustrates how badly we need tests for fusion (#229). So far our answer to question does it fuse is "hopes and prayers" which is clearly not enough

@Shimuuar Shimuuar added this to the 0.13 milestone Jun 11, 2020
@Bodigrim

Copy link
Copy Markdown
Contributor

@gksato could you possibly submit a test for this, using inspection-testing?

@gksato

gksato commented Jun 12, 2020 •

Copy link
Copy Markdown
Contributor Author

@Shimuuar Now I get the situation well. Just rewriting back

length = Bundle.length . stream

does work with GHC 8.8.3. However it doesn't work with GHC 8.0.2, the version that was current at the time a811a86 was introduced, since there is, formally, a loop among length, clone, unsafeCopy, stream:

-- | /O(1)/ Yield the length of the vector
length :: Vector v a => v a -> Int
{-# INLINE length #-}
length = Bundle.length . stream

-- | /O(1)/ Convert a vector to a 'Bundle'
stream :: Vector v a => v a -> Bundle v a
{-# INLINE_FUSED stream #-}
stream v = Bundle.fromVector v

{-# RULES
...

"New.unstream/stream [Vector]" forall v.
  New.unstream (stream v) = clone v

...  #-}

-- | Convert a vector to an initialiser which, when run, produces a copy of
-- the vector.
clone :: Vector v a => v a -> New v a
{-# INLINE_FUSED clone #-}
clone v = v `seq` New.create (
  do
    mv <- M.new (length v)
    unsafeCopy mv v
    return mv)

-- | /O(n)/ Copy an immutable vector into a mutable one. The two vectors must
-- have the same length. This is not checked.
unsafeCopy
  :: (PrimMonad m, Vector v a) => Mutable v (PrimState m) a -> v a -> m ()
{-# INLINE unsafeCopy #-}
unsafeCopy dst src = UNSAFE_CHECK(check) "unsafeCopy" "length mismatch"
                                         (M.length dst == length src)
                   $ (dst `seq` src `seq` basicUnsafeCopy dst src)

Since no New.unstream appears at the inline sites of length, this loop is only formal and not actual; I guess that's why reverting a811a86 worked with GHC 8.8.3. However older GHCs seemingly doesn't detect the fact that the loop is not essential.

@gksato

gksato commented Jun 12, 2020

Copy link
Copy Markdown
Contributor Author

@Bodigrim I'm skimming through inspection-testing and #229. Wait a moment...

@Shimuuar

Copy link
Copy Markdown
Contributor

Copy/unsafeCopy/clone require vector to be materialized anyway. So we don't lose anything by switching to basicLength. LGTM

@gksato Could you please add info from #307 (comment) to commit message?

@Shimuuar

Copy link
Copy Markdown
Contributor

@Bodigrim @lehins what do you think about this PR?

@gksato

gksato commented Jun 20, 2020

Copy link
Copy Markdown
Contributor Author

@gksato Could you please add info from #307 (comment) to commit message?

@Shimuuar Oops. I've forgotten to check this PR. Can I reword the commit and force-push? I'll not change the content.

@Shimuuar

Copy link
Copy Markdown
Contributor

Please do

This change makes fusible 'Data.Vector.Generic.length',
which currently can be inlined but doesn't fuse well.
Before this revision, 'length' uses non-fusible 'stream''.
This revision replaces 'stream'' with fusible 'stream',
and at the same time substitute the mutually-recursive
occurrences of 'length' with 'basicLength'.

Rationale:
The current definition was introduced in the commit
a811a86.
Prior to that commit,
'length' could not even be inlined with older GHCs,
which elected 'length' to be a loop-breaker due to the cyclic
references among 'clone', 'unsafeCopy', 'length' and 'stream':
* 'clone' refers to 'length' and 'unsafeCopy' in its definition;
* 'unsafeCopy' refers to 'length' in its definition;
* 'length' referred to 'stream' in its definition;
* 'stream' refers to none of the above in its definition,
  but the rule 'New.unstream/stream' rewrites
  @'New.unstream' ('stream' v)@ with @'clone' v@.
This failure of inlining was reported as
haskell#97.

To be precise, this cycle of references is immaterial with newer
GHCs like GHC 8.8.3.
Since 'New.unstream' does not occur in the definitions of
'clone', 'unsafeCopy', and 'length',
indefinite inlining will not happen anyway.
GHC 8.8.3 seems to detect this immateriality of the
referencial loop in question and refrains from setting 'length' as a
loop-breaker, even with
acdcff3
(the parent commit of a811a86).
However with older GHCs, including GHC 8.0.2, regarded
the cyclic references fatal and marked 'length' as a loop-breaker.
Note that GHC 8.0.2 was current at the time a811a86 was introduced.

The commit a811a86 resolved this by replacing 'stream'
with 'stream'' in the definition of 'length'. The function
'stream' refers to 'clone' in its fusion rule, but 'stream''
doesn't possess any rule. This cut open the cycle of references
and made 'length' inlinable.

However, since 'stream'' doesn't possess any rule, defining
'length' = 'Bundle.length' . 'stream''
is all the same as setting
'length' = 'basicLength'.
This prevents any stream fusion from happening.

This commit resolves this problem by resetting 'length' back
to be @'Bundle.length' . 'stream'@, and instead replacing
cyclic occurrence of 'length' with 'basicLength'
(Just *inlined the simplification result of the definition*).
Now 'length' is both inlinable and fusible, even with older GHCs.

Fixes: haskell#306
See also: haskell#97
  haskell#111
  haskell#155
@gksato

gksato commented Jun 20, 2020

Copy link
Copy Markdown
Contributor Author

@Shimuuar I've done it!

@Shimuuar

Copy link
Copy Markdown
Contributor

Did you look into inspection-testing by chance? Adding even single test would be great contribution since adding more will become relative straightforward.

@lehins
lehins self-requested a review June 20, 2020 08:24

@lehins lehins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I agree with @Shimuuar any sort of test case for this functionality would be awesome.

@gksato

gksato commented Jun 20, 2020 •

Copy link
Copy Markdown
Contributor Author

In fact, I was just puzzled by the tests in #229, because they include test_length and the comment reports that all the tests were successful. I now noticed that test_length was just defined as a test and was not included in the test to be executed.

@gksato

gksato commented Jun 21, 2020 •

Copy link
Copy Markdown
Contributor Author

Here's a small fusion test for this pull request. I picked up @nomeata's work on #229 and modified a bit:

I wasn't really sure it was OK to merge a big mass of tests with this pull request, I left it separate branches. If you know a better-looking way to present these to you, please let me know!

@Shimuuar
Shimuuar merged commit 8447ec0 into haskell:master Jun 21, 2020
@Shimuuar

Copy link
Copy Markdown
Contributor

I've merged PR, thanks!

It would be better to submit tests as separate PR. It will probably require several iterations to get into working order and I don't think it makes sense to keep this PR unmerged.

P.S. Didn't look at tests yet

@gksato
gksato deleted the fusible-length branch June 22, 2020 09:05
@gksato
gksato restored the fusible-length branch June 22, 2020 11:54
lehins pushed a commit that referenced this pull request Jan 16, 2021
This change makes fusible 'Data.Vector.Generic.length',
which currently can be inlined but doesn't fuse well.
Before this revision, 'length' uses non-fusible 'stream''.
This revision replaces 'stream'' with fusible 'stream',
and at the same time substitute the mutually-recursive
occurrences of 'length' with 'basicLength'.

Rationale:
The current definition was introduced in the commit
a811a86.
Prior to that commit,
'length' could not even be inlined with older GHCs,
which elected 'length' to be a loop-breaker due to the cyclic
references among 'clone', 'unsafeCopy', 'length' and 'stream':
* 'clone' refers to 'length' and 'unsafeCopy' in its definition;
* 'unsafeCopy' refers to 'length' in its definition;
* 'length' referred to 'stream' in its definition;
* 'stream' refers to none of the above in its definition,
  but the rule 'New.unstream/stream' rewrites
  @'New.unstream' ('stream' v)@ with @'clone' v@.
This failure of inlining was reported as
#97.

To be precise, this cycle of references is immaterial with newer
GHCs like GHC 8.8.3.
Since 'New.unstream' does not occur in the definitions of
'clone', 'unsafeCopy', and 'length',
indefinite inlining will not happen anyway.
GHC 8.8.3 seems to detect this immateriality of the
referencial loop in question and refrains from setting 'length' as a
loop-breaker, even with
acdcff3
(the parent commit of a811a86).
However with older GHCs, including GHC 8.0.2, regarded
the cyclic references fatal and marked 'length' as a loop-breaker.
Note that GHC 8.0.2 was current at the time a811a86 was introduced.

The commit a811a86 resolved this by replacing 'stream'
with 'stream'' in the definition of 'length'. The function
'stream' refers to 'clone' in its fusion rule, but 'stream''
doesn't possess any rule. This cut open the cycle of references
and made 'length' inlinable.

However, since 'stream'' doesn't possess any rule, defining
'length' = 'Bundle.length' . 'stream''
is all the same as setting
'length' = 'basicLength'.
This prevents any stream fusion from happening.

This commit resolves this problem by resetting 'length' back
to be @'Bundle.length' . 'stream'@, and instead replacing
cyclic occurrence of 'length' with 'basicLength'
(Just *inlined the simplification result of the definition*).
Now 'length' is both inlinable and fusible, even with older GHCs.

Fixes: #306
See also: #97
  #111
  #155
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Data.Vector.Generic.length not fusible

5 participants