Skip to content

Skip SourceSpan in Binder Eq, Ord for faster exhaustivity check#3265

Merged
kritzcreek merged 3 commits intopurescript:masterfrom
bitemyapp:bitemyapp/dont-compare-source-span
Mar 6, 2018
Merged

Skip SourceSpan in Binder Eq, Ord for faster exhaustivity check#3265
kritzcreek merged 3 commits intopurescript:masterfrom
bitemyapp:bitemyapp/dont-compare-source-span

Conversation

@bitemyapp
Copy link
Copy Markdown
Contributor

@bitemyapp bitemyapp commented Mar 5, 2018

I get a speedup of about 17x for LargeSumType.purs from this change. My curiosity was piqued by something @parsonsmatt mentioned while he was doing something unrelated.

Optimized run went from 5 seconds to 0.29 seconds, profiled run went from 17 seconds to 1 second.

Here's a dump of some of what I was looking while optimizing this:

Before optimizing:

compare                 Language.PureScript.AST.SourcePos     src/Language/PureScript/AST/SourcePos.hs:53:25-27               28.3    0.0
>>=                     Text.Parsec.Prim                      Text/Parsec/Prim.hs:202:5-29                                    18.1   10.8
>>=.\.succ'             Data.Attoparsec.Internal.Types        Data/Attoparsec/Internal/Types.hs:146:13-76                      6.7    1.7
compareText.go          Data.Text                             Data/Text.hs:(416,5)-(422,33)                                    6.4    0.0
parsecMap.\             Text.Parsec.Prim                      Text/Parsec/Prim.hs:190:7-48                                     4.2    6.2

I found this flamegraph more helpful:

hmhq9xa

Big props to @MonoidMusician for pointing me in the right direction here.

Zooming in a couple parts of the main left block of the flamegraph:

screenshot from 2018-03-05 00-02-29

screenshot from 2018-03-05 00-02-20

After writing the custom Ord instance:

screenshot from 2018-03-05 01-25-00

It's mostly disappeared from the flamegraph. Based on this and the human-readable .prof it seems like time is getting dominated by Parsec now.

Normally I have moral objections to manual instances varying from derived instances but it isn't clear that incorporating the SourceSpan into Binder's Ord or Eq comparisons would serve any purpose. I don't have a lot of choice with the exhaustivity checking using Ord instances as-written because there ordNubBy isn't a thing for what are obvious reasons if you look at the implementation of ordNub. Any options for hacking around this seem more odious than the custom Eq/Ord. The speedup was purely from the Ord instance but I did the Eq instance as well to harmonize its behavior with Ord.

@LiamGoodacre
Copy link
Copy Markdown
Member

Thanks!
I've restarted the Travis builds that timed out.

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Mar 5, 2018

Nice result!

I'm wondering if it's be worth factoring this into the derived compare composed with a function which strips source spans. It'd mean two traversals, but you'd get a function which would probably be useful elsewhere (for similar optimizations or other things). If the generic annotations work gets merged later, it'd literally become an fmap or something similar.

Also please don't forget to update contributors.md.

@bitemyapp
Copy link
Copy Markdown
Contributor Author

I've added myself to CONTRIBUTORS.md, thank you!

I'm wondering if it's be worth factoring this into the derived compare composed with a function which strips source spans. It'd mean two traversals, but you'd get a function which would probably be useful elsewhere (for similar optimizations or other things). If the generic annotations work gets merged later, it'd literally become an fmap or something similar.

I'd hesitate for a couple reasons:

This is personal preference and I realize I am probably slower to do this than is typical for the average PureScript contributor, but I don't like generalizing until I have at least 2 or 3 applications of the generalization I can evaluate. Is there an example anywhere else in the test suite of compare being expensive? If there is, I'd be happy to poke at deduplicating. For the moment, it's extra work incorporating an approach I'm not terribly familiar with. I prefer not to add code that isn't yet needed to the compiler.

@parsonsmatt
Copy link
Copy Markdown
Contributor

Looking at the exhaustiveness algorithm, we might be able to make it even more optimal if we use a Set instead of a list that we ordNub at every step of the computation.

@bitemyapp
Copy link
Copy Markdown
Contributor Author

bitemyapp commented Mar 5, 2018

@parsonsmatt that was the first approach I investigated and I think it was a good idea not to tackle that first. That's what I'm giving @MonoidMusician credit for helping me to avoid.

Here's the current state of the flamegraph:

screenshot from 2018-03-05 14-58-36

See the skinny'ish tall tower on the left?

Zooming in, it's still ordNub eating us alive in the exhaustiveness check:

screenshot from 2018-03-05 14-58-28

But it's still principally compare, this time on Name, that is killing us. At time of writing:


-- | A sum of the possible name types, useful for error and lint messages.
data Name
  = IdentName Ident
  | ValOpName (OpName 'ValueOpName)
  | TyName (ProperName 'TypeName)
  | TyOpName (OpName 'TypeOpName)
  | DctorName (ProperName 'ConstructorName)
  | TyClassName (ProperName 'ClassName)
  | ModName ModuleName
  | KiName (ProperName 'KindName)
  deriving (Eq, Ord, Show, Generic)

I think eliminating the nested lists of binders would be worth it iff it is the case that the number of times values are compared can be dramatically reduced by keeping them in a Set to begin with. It's possible a naive translation to Set [Binder] could suffice for that. I started a (messy) branch for this last night: https://github.com/bitemyapp/purescript/tree/callen/abortive-set-exhaustive-ordnub

I'm going to try to keep the change localized to the exhaustiveness check but it still seems like a bit of a briar patch. I'll poke more but I'd like to see this change get in independent of anything else as it seems like an easy and big win.

@kritzcreek
Copy link
Copy Markdown
Member

One suggestion I'd like to think about is if we could just make Eq and Ord on SourceSpans constantly return Eq. I doubt we ever want these spans to affect our equality checks.

If that's too radical, maybe we can use a newtype wrapper that skips comparisons for usage within our AST data types.

@bitemyapp
Copy link
Copy Markdown
Contributor Author

@kritzcreek I think that's a bad idea that doesn't have a GHC profile motivating it. If you don't actually want people to use an Eq or Ord instance, stop deriving them and fix all the code that implicitly depended upon them. Doing surprising things at runtime doesn't seem to me to be consistent with the design ethic I see in Haskell or PureScript.

I do not think exceeding the changes of this PR WRT SourceSpans is a good use of time unless someone can point out an example of the source location information causing a performance problem elsewhere.

@kritzcreek
Copy link
Copy Markdown
Member

Okay, yeah I think that's probably right. I was thinking about our Declaration type, but I'd first need a profile to actually show it causing problems. LGTM from me 👍

@kritzcreek kritzcreek merged commit d73957d into purescript:master Mar 6, 2018
@bitemyapp bitemyapp deleted the bitemyapp/dont-compare-source-span branch March 6, 2018 20:00
@puffnfresh
Copy link
Copy Markdown
Contributor

I'd value a comment about how the derived instance is different.

@bitemyapp
Copy link
Copy Markdown
Contributor Author

bitemyapp commented Mar 7, 2018

@puffnfresh Reasonable even if it's obvious what we're doing (dropping the source spans from the comparisons) -- rather leave a note explaining the purpose of the custom instance for posterity. I'll PR it later tonight.

@bitemyapp
Copy link
Copy Markdown
Contributor Author

@puffnfresh #3271

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants