-
Notifications
You must be signed in to change notification settings - Fork 28
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
More small parse performance improvements #229
More small parse performance improvements #229
Conversation
) | ||
, Core.lazy | ||
(\() -> | ||
Core.succeed |
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.
Hmm, I wonder if this is faster. As far as I know Parser.lazy
is only there to avoid cyclic references between parsers, not to improve performance. Maybe it does as well but I'm not sure 🤔
Have you run benchmarks on this change alone, or only on the whole PR?
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.
map needs to check whether it's valid or not underneath, so this kind of makes sense to me
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.
Oh right, that's a good point. But to do that it also re-defines the parser everytime it gets evaluated, whereas with map
the parser is already defined, and (from prior optimizations) that was quite bad for performance.
I really don't know what is faster though. I'm okay with taking this change, and then benchmarking this change to know what is faster, because this would be a cool learning.
src/Elm/Parser/Expression.elm
Outdated
|
||
operation100 : Node Expression -> Parser State (Node Expression) | ||
operation100 leftExpression = | ||
operation 100 leftExpression |
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.
How much faster is this? Because it seems a little excessive :)
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.
indeed, benchmarking rn. There are likely more low-hanging fruits (e.g. the List.map inside oneOf there that can be moved into the filterMap one level higher or moving laziness to only the parts that need it instead of the whole parser)
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.
benchmarks show predefining these doesn't make a difference, you're right.
A bit strange though, because that means using a Dict or case is unnecessary...
Maybe there is a way to go through the filterMap early, should be, right? benchmarking. . .
Yeah, early filterMap is a bit faster, consistently about 5% (I wonder why only a bit)
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 the way it's done right now, we're not actually precomputing anything, and instead recomputing (filter the precedences) every time. I'm slightly in doubt as to whether precomputation worked before this PR either, though I at some believed I had made it work.
If we got rid of the expression argument in operator
(probably not possible?), or if we did the precedence filtering and then returned a lambda that took the expression argument, then it would work for sure.
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.
are you looking at the latest state where we have module-level filtered lists?
And I don't think it worked in the Dict
version because the filterMap is inside the constructed lambda for each entry. But I haven't tested so who knows
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.
Sorry, what do you mean with "module-level filtered lists"?
And yeah, I don't think it worked with Dict
either, although at the time I did think so. I'll look into it at some point again.
List.filterMap is optimized in a non-merged PR (mdgriffith/elm-optimize-level-2#75). But it does get optimized in elm-review (which is not using eol2, just doing a few similar patches). Random optimization idea as well (I'll reply to your other questions tomorrow morning): we do a LOT of string concatenations when parsing string literals. But most of the time we can get the contents through a simple get chompedstring. The only times we can't do that is when there's escaping (which is not the common case) and even then we could probably use getChompedString for the rest of the strings, leading to a lot less concatenations. |
I don't think this will improve anything because literals that don't contain escaped characters are already parsed fast by Keep the random ideas flowing. |
Yeah, we already do that, I only remembered the part where we add one character. Too bad 😅
I think that's a good idea. I'm honestly confused as to how this even works, because my mental model says that the first For the other location (
I don't yet understand the differences between symbol/token/keyword. I'm fine with changing to the fastest one. And maybe having some guidance written down somewhere to explain which one to use in general (or in which case).
In terms of breaking changes, I think it's fine. Do you know if it's faster? It removes an intermediary function that does the same thing, but maybe that function would get optimized by the JIT. Those are the questions I pondered and didn't know the answer yet.
I don't have any useful tool for this unfortunately, just individual smaller benchmarks like the ones you made (but which I haven't used recently, do you have one somewhere that I could use as a starting point?) and @jiegillet's 100 run benchmark.
I'm confused as to the final results for this, what is finally the fastest? And is this a pattern we use often? I only see it in |
Core.variable is consistently ~15% faster than with symbol |. chompWhile. I imagine it could even be faster than regular
I pushed my benchmark code to this branch: https://github.com/lue-bird/elm-syntax/tree/specific-and-full-benchmarks/benchmark
The doc explains it well: symbol = token and keyword only parses successfully if the character after the symbol is whitespace. That's really useful to e.g. not think that importStatement = ... should be committed as an import. (would be equivalent to something like
That's the cool thing about Another good improvement by checking for Latin first letters first: Another random idea: Core.succeed identity
|. Core.symbol lp
|= p state
|. Core.symbol rp
-->
Core.map (\() -> identity)
(Core.symbol lp)
|= p state
|. Core.symbol rp (same with A fun experience: I had a shock moment after changing a oneOf order where something fishy was happening (consistently) A failed experiment: I created a module-level alias for |
…lifiedPatternArgumentList
…re less likely than type and type alias
5b6a835
to
b70e7a0
Compare
This has been amazing @lue-bird, I can't believe you've pretty much made the parser twice as fast 🤯 I don't see any reason not to merge this as it is. Feel free to continue the performance improvements in new PRs, if you still have the performance itch 😄 (Note: I've cleaned up the branch and force-pushed it to merge it nicely.) |
running the benchmarks with eol2 --optimize-speed in chromium
(the improvements are pretty consistent now)
Done by
succeed () |> map (\() -> x)
tolazy (\() -> succeed x)
succeed a |. x
tomap (\() -> a) x
Dict
foroperations
tocase
(and actually pre-compute available operations for all precedences)Not all changes are necessarily performance related. Some change ignored unit arguments to
\() ->
. One fixes import alias parsing.