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

wasmparser: Remove offset param in VisitOperator #804

Merged
merged 4 commits into from Oct 27, 2022

Conversation

nagisa
Copy link
Contributor

@nagisa nagisa commented Oct 26, 2022

Implementing VisitOperator is complicated by the additional passing-through of the offset everywhere. A much simpler alternative code wise is to stash the offset away within the implementation of VisitOperator before invoking SomeReader::visit_operator.

This raises some complications with visitors that depend on the offsets and rely on them for functional correctness, though. This is the case for the FuncValidator which uses offsets to verify that the very last end operator seen is at the same offset as the reader when the validator is finalized.

self.validator.with_resources(&self.resources)
.$visit(offset $($(,$arg)*)?)
fn $visit(&mut self $($(,$arg: $argty)*)?) -> Result<()> {
// TODO: whatever happens if the caller does not set `op_offset` ahead of time?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The public VisitOperator implementation on this type is problematic. The validation today depends on offsets always getting set-up correctly for more than just the error messages. In particular it sets self.end_which_emptied_control to the offset of the very last end for the function, which is then checked in the finish method.

On the other hand, it is possible for the users to call reader.visit_operator(func_validator) while forgetting to set-up the op_offset. This is distinct from FuncValidator::op which does the right thing.

I would say resolving this will require to remove or move this implementation to some other type which forces callers to set up the offset one way or another, much like how with_resources below does.

Maybe something like

fn visit_operator(&self, reader: BinaryReader) -> ... { }

Copy link
Member

Choose a reason for hiding this comment

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

Hm yeah that is indeed unfortunate. It's fine to implement the end_which_emptied_control bit entirely differently, that was just the first thing I reached for.

One possibility, though, could perhaps be a default-does-nothing trait method which is along the lines of "I'm about to start visiting this offset", which could be called at the start of visit_operator and ignored by almost everything except the validator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, adding a visit_offset or such to the VisitOperator is an option. It is an obvious way forward, but it isn’t clear to me if it is the best way forward quite yet.

I want to give it some thought and maybe experiment with them to see how things work out if done differently.

Copy link
Contributor Author

@nagisa nagisa Oct 27, 2022

Choose a reason for hiding this comment

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

I ended up with what I think is a simpler approach for now of adding a visitor method to the FuncValidator which returns an opaque impl VisitOperator. This is basically what all of the methods were delegating to anyway.

@Robbepop
Copy link
Contributor

Robbepop commented Oct 27, 2022

The main motivation behind the VisitOperator API was as a more efficient way to parse, validate and transform Wasm bytecode. I can understand the motivation behind this PR but really hope that we won't regress performance of validation or other means of using this API.
Originally I proposed the VisitOperator trait with an Input associated type that allowed for a generic first parameter instead of a offset: usize one to model other usages. I see this design as an alternative.

Implementing `VisitOperator` is complicated by the additional
passing-through of the `offset` everywhere. A much simpler alternative
code wise is to stash the `offset` away within the implementation of
`VisitOperator` before invoking `SomeReader::visit_operator`.

This raises some complications with visitors that depend on the offsets
and rely on them for functional correctness, though. This is the case
for the `FuncValidator` which uses offsets to verify that the very last
`end` operator seen is at the same offset as the reader when the
validator is finalized.
This type depends on `offset`s being passed in correctly, that’s how it
validates whether the last `End` operator is actually the end of the
function.

Prior to removal of the `offset` parameters, they’d be correct by the
definition of `reader` passing the offset in. However, with move of this
responsibility to the user code, it now became straightforward to invoke
the visitor methods in a way that violates the assumptions in the
implementation of the `FuncValidator`.

Instead, in order to obtain a visitor, users are now required to call a
method which also sets up the offsets accordingly.
@nagisa
Copy link
Contributor Author

nagisa commented Oct 27, 2022

The main motivation behind the VisitOperator API was as a more efficient way to parse, validate and transform Wasm bytecode. I can understand the motivation behind this PR but really hope that we won't regress performance of validation or other means of using this API.

I definitely am aware of that. I wouldn’t have proposed this change if my intuition didn’t suggest the different ways of passing the offset around didn’t meaningfully change the perf characteristics of the code.


My dev machine isn’t really set-up for quality benchmarking, with a ton of stuff running in there, but here are the results:

parse/tests             time:   [2.5424 ms 2.5431 ms 2.5438 ms]                         
                        change: [+1.2777% +2.1963% +2.9292%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

validate/tests          time:   [9.2935 ms 9.2974 ms 9.3014 ms]                           
                        change: [-1.5510% -1.1343% -0.8100%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

validate/bz2            time:   [953.63 µs 954.91 µs 956.74 µs]                         
                        change: [-0.2498% -0.1441% -0.0084%] (p = 0.02 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe

parse/bz2               time:   [578.27 µs 578.31 µs 578.35 µs]                      
                        change: [+0.9696% +1.0978% +1.1982%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 7 outliers among 100 measurements (7.00%)
  6 (6.00%) high mild
  1 (1.00%) high severe

validate/intgemm-simd   time:   [1.9817 ms 1.9839 ms 1.9867 ms]                                   
                        change: [+0.6718% +0.7915% +0.9672%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high severe

Benchmarking parse/intgemm-simd: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 5.6s, enable fla
t sampling, or reduce sample count to 60.
parse/intgemm-simd      time:   [1.1027 ms 1.1030 ms 1.1035 ms]                                
                        change: [+1.8445% +1.9715% +2.0582%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) low mild
  3 (3.00%) high mild
  7 (7.00%) high severe

validate/pulldown-cmark time:   [2.2530 ms 2.2542 ms 2.2555 ms]                                     
                        change: [+0.8914% +0.9679% +1.0370%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

Benchmarking parse/pulldown-cmark: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.7s, enable fla
t sampling, or reduce sample count to 60.
parse/pulldown-cmark    time:   [1.3279 ms 1.3281 ms 1.3285 ms]                                  
                        change: [+0.8563% +0.8996% +0.9471%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 16 outliers among 100 measurements (16.00%)
  1 (1.00%) low severe
  6 (6.00%) high mild
  9 (9.00%) high severe

Benchmarking validate/spidermonkey: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 5.7s, or reduce 
sample count to 80.
validate/spidermonkey   time:   [56.373 ms 56.653 ms 57.075 ms]                                  
                        change: [+0.9209% +1.4428% +2.2000%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high severe

parse/spidermonkey      time:   [33.288 ms 33.293 ms 33.299 ms]                               
                        change: [+0.7947% +0.8221% +0.8500%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
  8 (8.00%) high mild
  1 (1.00%) high severe

the benchmark runner is indeed reporting some regressions over the baseline, however where there are regressions, they are definitely still within run-to-run variance on my machine, as running the same benchmark will readily report an improvement of about as many percent.

@alexcrichton
Copy link
Member

This seems reasonable to me and I like the way it's integrated with FuncValidator. Was there anything else you wanted to add before merging?

For benchmarking using criterion is not useful to detect very small changes, which if this has any effect it'd be quite small. I would hope, though, that any slowdown could be recovered via other means rather than plumbing offset everywhere.

@nagisa
Copy link
Contributor Author

nagisa commented Oct 27, 2022

I think this is ready to merge at this point. I wasn’t thinking of making any significant additional changes in this PR, except for addressing any feedback.

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.

None yet

3 participants