-
Notifications
You must be signed in to change notification settings - Fork 46
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
chore(parser): optimize the most common lexer matches into lookup tables #814
Conversation
@@ -17,7 +17,7 @@ impl<'a> Token<'a> { | |||
} | |||
|
|||
/// Get a reference to the token's data. | |||
pub fn data(&self) -> &str { | |||
pub fn data(&self) -> &'a str { |
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 forgot about this, just a note here that this lifetime was always bound to the source document and not the token itself - it's fine to hold the data as long as the source is still around.
I'm not sure if this is a breaking change since it's technically more permissive than it was before but just fyi
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.
Good spot! If we thread this change through other parts of the code I think we can actually get rid of some intermediate allocations in the parser. I'll submit that in a follow-up 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.
I like the idea. I do kinda doubt the lexer is the most important part of apollo-rs to optimise (and currently big changes to improve performance are not our primary priority anyways), but a simple incremental improvement like this is always nice!
lut[b'w' as usize] = true; | ||
lut[b'x' as usize] = true; | ||
lut[b'y' as usize] = true; | ||
lut[b'z' as usize] = true; |
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.
If only ranged for
was available in const fn 😁
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 this could be a while
loop
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.
you can generate it in a macro if you dare :p
https://github.com/rust-bakery/parser_benchmarks/blob/master/http/nom-optimized/src/combinators.rs#L194-L242
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.
Something like this works:
const fn generate_table() -> [bool;256] {
let mut table = [false;256];
let mut i = b'a';
while i <= b'z' + 1 {
table[i as usize] = true;
i += 1;
}
let mut i = b'A';
while i <= b'Z' + 1 {
table[i as usize] = true;
i += 1;
}
table
}
Seems it's less obvious what's happening and can make the compiler hang if you make a mistake. const for
would be perfect, but requires unstable features. Also we don't want to make it too easy to make the whole lexer do this, other token types are alot less frequent and benchmarking/profiling shows there was no benefit in those cases.
In fact in some cases (I forgot which) it seemed to actually make things worse
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 agree writing this stuff out is simpler to understand than a while loop.
Overview
Basically just bastardizes the good ideas in https://github.com/maciejhirsz/logos in a modest change trying to get the most value without making the code unreadable or hard to maintain. Both functions internally call
is_ascii
to prevent overflows since we're dealing with utf-8.There's obviously more you could do but from my profiles this seems to be the most impactful paths without making the whole thing unreadable.
Feel free to close as there are obvious tradeoffs here but I opened for your consideration.
Impact
The impact of this change really depends on the document being parsed. For small documents its negligible but for large documents, particularly schema SDLs the change speeds things up to ~18%.
The delta on the public github schema was quite sizable: