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

Allow end users to observe JSX token's value #331

Merged
merged 1 commit into from
Aug 28, 2022
Merged

Conversation

adams85
Copy link
Collaborator

@adams85 adams85 commented Aug 26, 2022

A minor correction to #328

@adams85 adams85 force-pushed the fix/jsxtoken-value branch 4 times, most recently from 7bf06d3 to f9aa1ce Compare August 26, 2022 23:30
Comment on lines 121 to 135
public object? Value
{
TokenType.Template => ((TemplateHolder) _value!).Cooked,
TokenType.RegularExpression => ((RegexHolder) _value!).Value,
_ => _value
};
// When this accessor gets inlined, it has a significant negative impact on max. nesting depth (on .NET 6),
// which is kind of weird...
[MethodImpl(MethodImplOptions.NoInlining)]
get => _value is ValueHolder holder ? holder.Value : _value;
}
Copy link
Collaborator Author

@adams85 adams85 Aug 26, 2022

Choose a reason for hiding this comment

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

@lahma Any idea on this?

I had to add MethodImplOptions.NoInlining to pass the new max. nesting depth test. I assume that the runtime started to inline the accessor automatically because it got shorter. I suspected that the local variable messed things up but changing it to something like _value is ValueHolder ? ((ValueHolder) _value).Value : _value; made no difference.

How come an inlined method uses more stack space than a non-inlined one?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well at least this is now more complex for the runtime. Earlier was quick jump for regular case (no value holder) without type checks. Now this always incurs more expensive type check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately, this doesn't answer the question: I tested the earlier version with forced inlining (AggressiveInlining applied to the getter of Value). The result is the same: inlining Value reduces max. nesting depth to around 430. It just hasn't turned out before because it seems that the runtime doesn't tend to inline the earlier version without explicitly telling it to do so, as opposed to the new one.

Well at least this is now more complex for the runtime. Earlier was quick jump for regular case (no value holder) without type checks. Now this always incurs more expensive type check.

This, of course, is true but according to the above it may only affect execution time. I'll do some benchmarks later to ensure that we don't have a regression concerning that. We can revert to switch if that proves to be faster.

Copy link
Contributor

Choose a reason for hiding this comment

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

While reading Pro .NET Memory Management yesterday, I noticed a small mention at some point suggesting that AggressiveInlining may actually disable a bunch of optimizations. It made me think of this. Any chance that forcing inlining disables layout optimizations of the struct, or similar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I noticed a small mention at some point suggesting that AggressiveInlining may actually disable a bunch of optimizations

Basically, inlining should enable a lot of optimizations which aren't possible otherwise, not prevent them. Especially, on .NET 6: see this excellent article by Stephen Toub on the topic.

I suppose the author of Pro .NET Memory Management is talking about this:

There’s also the impact on the JIT compiler itself, as the JIT has limits on things like the size of a method before it’ll give up on optimizing further; inline too much code, and you can exceed said limits.

But this goes for the method into which we inline other methods, not for the methods we inline.

Any chance that forcing inlining disables layout optimizations of the struct, or similar?

I doubt that inlining would affect the memory layout of structs. After some experimenting, I came to the conclusion that the problem lies around downcasting and accessing the Value property of the downcast holder object. After moving that part into a separate, non-inlined method, inlining started to work without extra stack consumption (see my later comment). I haven't had the patience to unearth the exact reasons but that's for sure we're pushing the limits of the runtime's capabilities here.

Comment on lines +124 to +134
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
// NOTE: This condition must not be inverted, otherwise the runtime (.NET 6) fail to inline the accessor correctly.
return Type is not (TokenType.RegularExpression or TokenType.Template or TokenType.Extension)
? _value
: GetValueFromHolder(in this);

[MethodImpl(MethodImplOptions.NoInlining)]
static object? GetValueFromHolder(in Token token) => ((ValueHolder) token._value!).Value;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Finally I've managed to find a combination which the runtime can inline without penalty.

If you change anything in this code (even if just invert the condition), inlining goes south and the acceptance test starts to fail. Mind-boggling...

However, this looks to work reliably and according to the benchmarks, we even have a little bit of improvement in execution time:

Esprima.Benchmark.FileParsingBenchmark

Diff Method FileName Mean Error Gen 0 Gen 1 Gen 2 Allocated
Old ParseProgram angular-1.2.5 17.628 ms 2.5634 ms 718.7500 343.7500 - 4,315 KB
New 17.475 ms (-1%) 1.1170 ms 718.7500 (0%) 312.5000 (-9%) - 4,315 KB (0%)
Old ParseProgram backbone-1.1.0 2.408 ms 0.2234 ms 136.7188 39.0625 - 676 KB
New 2.338 ms (-3%) 0.2539 ms 140.6250 (+3%) 35.1563 (-10%) - 676 KB (0%)
Old ParseProgram jquery-1.9.1 14.161 ms 0.6347 ms 625.0000 296.8750 - 3,764 KB
New 13.805 ms (-3%) 1.0131 ms 609.3750 (-3%) 296.8750 (0%) - 3,764 KB (0%)
Old ParseProgram jquery.mobile-1.4.2 22.922 ms 0.3971 ms 1031.2500 500.0000 31.2500 5,798 KB
New 22.039 ms (-4%) 2.4387 ms 1031.2500 (0%) 500.0000 (0%) 31.2500 (0%) 5,798 KB (0%)
Old ParseProgram mootools-1.4.5 11.000 ms 0.1603 ms 500.0000 250.0000 - 3,091 KB
New 10.776 ms (-2%) 0.4187 ms 500.0000 (0%) 250.0000 (0%) - 3,091 KB (0%)
Old ParseProgram underscore-1.5.2 2.022 ms 0.0332 ms 117.1875 42.9688 - 571 KB
New 1.930 ms (-5%) 0.0355 ms 113.2813 (-3%) 42.9688 (0%) - 571 KB (0%)
Old ParseProgram yui-3.12.0 10.352 ms 1.6380 ms 468.7500 234.3750 - 2,856 KB
New 9.828 ms (-5%) 1.0396 ms 468.7500 (0%) 234.3750 (0%) - 2,856 KB (0%)

@adams85 adams85 merged commit 0b89266 into main Aug 28, 2022
@adams85 adams85 deleted the fix/jsxtoken-value branch August 28, 2022 11:25
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