More small parse performance improvements#229
Conversation
| ) | ||
| , Core.lazy | ||
| (\() -> | ||
| Core.succeed |
There was a problem hiding this comment.
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.
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.
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.
|
|
||
| operation100 : Node Expression -> Parser State (Node Expression) | ||
| operation100 leftExpression = | ||
| operation 100 leftExpression |
There was a problem hiding this comment.
How much faster is this? Because it seems a little excessive :)
There was a problem hiding this comment.
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.
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.
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.
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.
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 |. xtomap (\() -> a) xDictforoperationstocase(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.