Skip to content
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

[RFC007] Migrate the parser to the new AST #2083

Merged
merged 23 commits into from
Nov 27, 2024
Merged

[RFC007] Migrate the parser to the new AST #2083

merged 23 commits into from
Nov 27, 2024

Conversation

yannham
Copy link
Member

@yannham yannham commented Oct 29, 2024

Migrate the parser to the new AST

Following the step by step implementation of RFC07, this PR migrates the parser to output the new AST, The plan is to convert this to the old AST for the remaining of the pipeline (typechecking, transformations, and evaluation), and to measure if that conversion is noticeable on various type of examples (small, big, small but importing big contracts, libraries, etc.)

Reviewing

The diff is quite big, but a lot of it is mostly mechanical changes. In particular I'm not sure that reviewing the change of the grammar.lalrpop file is really that interesting. Changes to bytecode::ast, bytecode::ast::compat and other modules are probably worth looking into it, though. For parser::utils and parser::uniterm, I'm not sure: most of it is mechanical, but wasn't entirely trivial either, in particular the gymnastic around type variable fixing. The latter might benefit from a pair of eyes.

Perf impact

I've written a detailed report below about the performance impact of this PR. The TL;DR is that although ast conversion is taking up a surprisingly high chunk of the overall parsing time, the net result is in the few percent difference and thus in the noise threshold, given that I also had net improvements on some runs. I think this is thus reasonable to move forward, and the more of the pipeline we'll migrate to the new AST, the better the performance, given that the new AST is smaller and has better locality.

Copy link
Contributor

github-actions bot commented Nov 20, 2024

First stab at making the parser compatible with the new AST
representation (`bytecode::ast::Ast`). This is a heavy
refactoring which required to update most of `parser::uniterm` and
`parser::utils` as well as `grammar.lalrpop`.

The current version is far from compiling; fixing compiler errors is
planned in follow-up work.
As we move toward a bytecode compiler and a bytecode virtual machine, we
are replacing the left part of the pipeline with the new AST
representation.

The bytecode module was previously gated by an experimental feature,
thea idea being that this feature would enable the whole bytcode
compiler pipeline. However, for now, we only have a new AST
representation, and it's being used in the mainline Nickel parser (and
soon, in the typechecker, etc.). Thus we need access to the new AST
representation by default, and it doesn't make much sense to gate it
behind a feature.

We'll reintroduce the feature once we have a prototype compiler and a
bytecode virtual machine, when it will then make sense to use the
feature to toggle between the legacy tree-walking interpreter and the
new bytecode compiler.
@yannham
Copy link
Member Author

yannham commented Nov 25, 2024

Nickel AST conversion impact report

Here is a preliminary performance impact report of this change on small and larger examples.

Methodology

I did an end-to-end run of nickel eval <foo> --metrics 1>/dev/null (or sometimes nickel eval --field foo when relevant, for example for Organist) on a dev profile with metrics enabled, comparing master (f1c826d) and the HEAD of this PR (4641104).

Metrics gather runtime of part of the pipeline in milliseconds. Note that not all operations are measured, but the measures still cover most of the actual runtime.

Here is what's been measured:

  • runtime:ast_conversion: for the version of this PR, measure the time of calling from_ast on the root node after parsing to the new representation. This includes the conversion for the stdlib.
  • runtime:eval: measure the time taken for pure evaluation (vm.eval()).
  • runtime:parse:nickel: measure the time taken to call the parse_xxx methods for Nickel code (excluding JSON etc.). Note that for the version of this PR, this includes the AST conversion.
  • runtime:prepare_main: time taken to call prepare_eval on the main program. This includes parsing, typechecking, and program transformation. This excludes the preparation of the stdlib.
  • runtime:prepare_stdlib: time taken to call prepare_eval on the stdlib. This includes parsing, typechecking, and program transformation.
  • runtime:type_check: time taken by calling type_check on the root node of the AST.
  • total: the total is computed as prepare_main + prepare_stdlib + eval, as preparation includes most of the beginning of the pipeline (parsing and AST conversion included). Note that this isn't the actual whole runtime of the command, because some things aren't measured, but should account for most of it. This is just the total of the stuff that we measure.

So, there are some inclusions here: type_check <= prepare_main, ast_conversion <= parse:nickel <= prepare_main + prepare_stdlib. eval is disjoint from the rest.

Findings

It's both surprising and interesting that my initial expectations were somehow not entirely right. AST conversion is taking a large part of the overall parsing, around 55-65% consistently across all example size. Since very small examples are dwarfed by preparing the stdlib, and thus dominated by parsing it, this looks bad at first: we take a big hit on parsing on small examples.

However, it's not the case: when comparing with the version that doesn't perform AST conversion, this one actually sometimes perform better overall! In general the difference on small examples is very small and in the noise threshold (the order of a percent), and can be in both directions, both for the total runtime and when comparing parsing times. My interpretation is that on master, we're actually spending more than half of the parsing time allocating Rcs in the heap to build the current AST. The new AST seems to be very performant to build, so now this allocation cost is just mostly delayed to the AST conversion phase.

For bigger examples, the Mantis case is around +6% of parsing time, and +4.3% overall overhead. It's unique it that it has a very low evaluation time (it's almost a static config), and is thus still dominated somehow by parsing.

The other large examples are heavily dominated by evaluation (more than 95%), and only take a few percent for parsing, so this PR is even less relevant for the overall performance. I suspect the difference we can see (like +3.3% on OPL v2) is rather due to variability, as it seems pure evaluation is taking a small hit but there's no reason for it. As some examples are a bit long to run, I haven't done averaging or warming so we can expect a bit of volatility. Hopefully it's still enough to validate that this change doesn't seem to have much performance impact (and that the new AST might get us parsing time down by around 50% once we don't have to perform the conversion anymore!).

You'll find the detailed data below.

Small size programs

Arrays example

Change after introducing the new AST: -4,3%
Change to parsing only: -4,5%

Before this PR

runtime:eval: 12 (8.5%)
runtime:parse:nickel: 113 (80.7%)
runtime:prepare_main: 2 (1.4%)
runtime:prepare_stdlib: 126 (90.0%)
runtime:type_check: 0 (0.0%)

total: 140

After this PR

runtime:ast_conversion: 66 (49,2%)
runtime:eval: 12 (8,9%)
runtime:parse:nickel: 108 (80,5%)
runtime:prepare_main: 2 (1,4%)
runtime:prepare_stdlib: 120 (89,5%)
runtime:type_check: 0 (0,0%)

total: 134

Fibonacci example

Change after introducing the new AST: +0%
Change to parsing: +1 %

Before this PR

runtime:eval: 44 (26.1%)
runtime:parse:nickel: 110 (65.4%)
runtime:prepare_main: 0 (0.0%)
runtime:prepare_stdlib: 124 (73.8%)
runtime:type_check: 0 (0.0%)

total: 168

After this PR

runtime:ast_conversion: 67 (39,9%)
runtime:eval: 44 (26,1%)
runtime:parse:nickel: 109 (64,8%)
runtime:prepare_main: 0 (0,0%)
runtime:prepare_stdlib: 124 (73,8%)

total: 168

GCC config example

Change after introducing the new AST: +1%
Change to parsing: -2%

Before this PR

runtime:eval: 13 (9.8%)
runtime:parse:nickel: 104 (78.1%)
runtime:prepare_main: 6 (4.5%)
runtime:prepare_stdlib: 114 (85.7%)
runtime:type_check: 2 (1.5%)

total: 133

After this PR

runtime:ast_conversion: 62 (45.5%)
runtime:eval: 19 (13.9%)
runtime:parse:nickel: 102 (75%)
runtime:prepare_main: 6 (4,4%)
runtime:prepare_stdlib: 111 (81,6%)

total: 136

Mid to large size programs

Change after introducing the new AST: -0.2%
Change to parsing only: +7.2%

Organist project.ncl

Before this PR

runtime:eval: 4161 (93.7%)
runtime:parse:nickel: 193 (4.3%)
runtime:prepare_main: 155 (3.4%)
runtime:prepare_stdlib: 124 (2.7%)
runtime:type_check: 20 (0.4%)

total measured: 4440

After this PR

runtime:ast_conversion: 142 (3.2%)
runtime:eval: 4142 (93.4%)
runtime:parse:nickel: 207 (4.6%)
runtime:prepare_main: 175 (3.9%)
runtime:prepare_stdlib: 117 (2.6%)
runtime:type_check: 20 (0.4%)

total measured: 4434

Mantis benchmark

8 Nickel files, 2356 kLoc

Change after introducing the new AST: +4.6%
Change to parsing only: +6.4%

Before this PR

runtime:eval: 111 (13.4%)
runtime:parse:nickel: 546 (66.0%)
runtime:prepare_main: 592 (71.5%)
runtime:prepare_stdlib: 124 (14.9%)
runtime:serialize: 0 (0.0%)
runtime:type_check: 80 (9.6%)

total measured: 827

After this PR

runtime:ast_conversion: 473 (54%)
runtime:eval: 112 (12.9%)
runtime:parse:nickel: 581 (67.1%)
runtime:prepare_main: 632 (73.0%)
runtime:prepare_stdlib: 121 (13.9%)
runtime:serialize: 0 (0.0%)
runtime:type_check: 81 (9.3%)

total measured: 865

OPL config test v1

67 Nickel files, 25 kLoC
1 JSON file, 15 kLoC

Change after introducing the new AST: +1%
Change to parsing only: +1.0%

Before this PR

runtime:eval: 80008 (97.9%)
runtime:parse:nickel: 791 (0.9%)
runtime:prepare_main: 1545 (1.8%)
runtime:prepare_stdlib: 125 (0.1%)
runtime:type_check: 280 (0.3%)

total measured: 81678

After this PR

runtime:ast_conversion: 454 (0.5%)
runtime:eval: 80522 (97.9%)
runtime:parse:nickel: 824 (1.0%)
runtime:prepare_main: 1603 (1.9%)
runtime:prepare_stdlib: 117 (0.1%)
runtime:type_check: 278 (0.3%)

total measured: 82242

OPL config test v2

165 Nickel files, 80 kLoC
1 JSON file, 15 kLoC

Change after introducing the new AST: +3.3%
Change to parsing only: +9.0%

Before this PR

runtime:eval: 310946 (97.1%)
runtime:parse:nickel: 4886 (1.5%)
runtime:prepare_main: 9149 (2.8%)
runtime:prepare_stdlib: 125 (< 0.1%)
runtime:type_check: 1956 (0.6%)

total measured: 320220

After this PR

runtime:ast_conversion: 3174 (0.9%)
runtime:eval: 310950 (96.7%)
runtime:parse:nickel: 5329 (1.6%)
runtime:prepare_main: 10198 (3.1%)
runtime:prepare_stdlib: 126 (< 0.1%)
runtime:type_check: 2399 (0.7%)

total measured: 321274

@yannham yannham marked this pull request as ready for review November 25, 2024 17:35
@yannham yannham requested a review from jneem November 25, 2024 17:40
Copy link
Member

@jneem jneem left a comment

Choose a reason for hiding this comment

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

Looking forward to the perf improvements from deleting the old ast!

core/src/parser/uniterm.rs Outdated Show resolved Hide resolved
core/src/parser/uniterm.rs Show resolved Hide resolved
@yannham yannham added this pull request to the merge queue Nov 27, 2024
Merged via the queue into master with commit 768e1d2 Nov 27, 2024
5 checks passed
@yannham yannham deleted the rfc007/parsing branch November 27, 2024 10:01
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.

2 participants