Create a different hierarchy of DynamicScope for use in startup interp + remove null checks in the existing hierarchy since they are redundant #4167

Closed
subbuss opened this Issue Sep 21, 2016 · 10 comments

Projects

None yet

3 participants

@subbuss
Contributor
subbuss commented Sep 21, 2016

The runtime now has a AddMissingInitsPass that the full interp and JIT modes use to add any required initializations of variables. This eliminates the need to do the same in DynamicScopes when lvars are looked up and they can be removed which might potentially lead to some perf. improvements by making DynamicScope code lean.

However, the startup interpreter won't have these IR-added inits in place and will still need the null checks. A good way to do that is add a different hierarchy there, and push those dynamic scopes for IR scopes that are running in startup mode. Maybe even a single 'multi-var-dynamic-scope' is sufficient for starters to evaluate what this yields. If startup impact is bad, the single scope can be specialized for different scenarios.

@headius
Member
headius commented Sep 21, 2016

Some ideas from discussing with Tom:

  • Initial move could be to add a new DynamicScope that supports arbitrarily-many variables (like ManyVars) and which does null checking. Remove null checking from the other scopes, since they'll only be made by Full and JIT.
  • Any methods that currently null check would need to keep null-checking, for non-Ruby code that doesn't have guarantees about initialization. Ruby code would call through new non-checked paths. This may be a way to avoid adding a new DynamicScope type at the cost of duplicating all current null-checking methods (i.e. Simple just uses existing null-checked methods, Full and JIT move to using unchecked methods).
  • It may be time we bought or built a library that can generate specialized collections from a template. For example, we don't want to have to maintain N DynamicScope classes in source for N widths of frame...better would be to have one template and generate those N classes at runtime as needed. This also allows us to potentially make JIT go directly at fields on those generated objects, something that was infeasible with full null checking.
@subbuss
Contributor
subbuss commented Sep 21, 2016

Any methods that currently null check would need to keep null-checking, for non-Ruby code that doesn't have guarantees about initialization. Ruby code would call through new non-checked paths. This may be a way to avoid adding a new DynamicScope type at the cost of duplicating all current null-checking methods (i.e. Simple just uses existing null-checked methods, Full and JIT move to using unchecked methods).

My thought as well.

@headius headius added a commit to headius/jruby that referenced this issue Sep 22, 2016
@headius headius Add paths to *DynamicScope that don't check nulls, offsets.
Our IR performes liveness checks on variables and should never
emit code that accesses variable indices outside the prescribed
scope size. This patch adds new getters that do not perform these
unnecessary checks, and wires up the JIT to call them.

This patch does not modify Full interpretation to use these
unchecked paths, because it would require either a boolean in
LocalVariaable operand and LoadLocalVariable instruction or a
pass to replace them with "Unchecked" versions.

It would also be better if we could generate these shapes of
DynamicScope on the fly, rather than maintaining a finite set of
mostly-identical subclasses.

See #4167.
a7399b7
@headius
Member
headius commented Sep 22, 2016

I've merged my PR that adds unchecked get* paths to all DynamicScope types. It also removes a scope type we no longer used, and fixes a null leak from Thread#name uncovered by the DynamicScope changes.

Note also that this PR only uses the unchecked paths from JIT, since Full IR still needs a way to indicate that a LocalVariable access should be unchecked. Currently LocalVariable.retrieve uses DynamicScope.getValue(offset, depth) and does its own null checking, and I believe some of the interpreter logic short-circuits the retrieve call and goes after DynamicScope directly.

@headius headius added this to the JRuby 9.1.6.0 milestone Sep 22, 2016
@headius
Member
headius commented Sep 22, 2016

@enebo How/where should we change full IR or full interpreter to use the unchecked paths?

@enebo
Member
enebo commented Sep 22, 2016

@headius I can change it. It is retrieveOp in InterpreterEngine but I need to make sure Startup is not using that version.

@headius
Member
headius commented Sep 23, 2016

You guys both asked me about perf. Here's a trivial bench:

[] ~/projects/jruby $ jruby -Xcompile.invokedynamic -e 'a = 0; b = 1; c = 2; d = 3; loop { t = Time.now; a = 0; while a < 100_000_000; a+=1; b, c, d = c, d, b; end; puts Time.now - t }'
1.851
1.659
1.544
1.682
1.466
1.449
1.438
^C
[] ~/projects/jruby $ rvm jruby-9.1.5.0 do jruby -Xcompile.invokedynamic -e 'a = 0; b = 1; c = 2; d = 3; loop { t = Time.now; a = 0; while a < 100_000_000; a+=1; b, c, d = c, d, b; end; puts Time.now - t }'
1.779
1.708
1.809
1.907
1.795
1.713
1.752
1.774
^C

This is a pretty good result, considering the loop itself takes a good portion of this:

[] ~/projects/jruby $ jruby -Xcompile.invokedynamic -e 'a = 0; loop { t = Time.now; a = 0; while a < 100_000_000; a+=1; end; puts Time.now - t }'
1.448
1.333
1.15
1.26
1.085
1.073
1.095
1.183
1.082
^C
[] ~/projects/jruby $ rvm jruby-9.1.5.0 do jruby -Xcompile.invokedynamic -e 'a = 0; loop { t = Time.now; a = 0; while a < 100_000_000; a+=1; end; puts Time.now - t }'
1.332
1.248
1.321
1.427
1.239
1.202
1.207
1.216
^C

So the three-variable swapping is maybe 0.37s on master+#4168 and 0.55s on 9.1.5.0, so that's around a 30% improvement. Not bad!

@enebo
Member
enebo commented Sep 23, 2016

@headius nice!

For those who follow our issues and want a little more background on impact...We try to eliminate heap variable scopes as much as we can. If a method has a closure in it then we end up generating a variable scope since we do not generally know whether that closure could end up becoming a binding. This change ends up improving variable access times whenever we have a heap scope. So if a method contains a closure it will likely see this improvement (it obviously may get watered down based on what else you do in that method). We do also create scopes for other reasons (like some native methods) but mainly you will notice the improvement in methods containing blocks.

@enebo
Member
enebo commented Sep 23, 2016 edited

For fun I ran the microbench against 1.7 with and without indy on and we are twice as fast in both modes as 1.7 now. I am not sure if that is because of our IR opts or because we now have some mandatory indy stuff wired in on 9k. Still, it is fun to see us improving relative to 1.7.x.

@subbuss
Contributor
subbuss commented Sep 23, 2016

Yes, good results. Better than I anticipated even.

@headius headius added a commit to headius/jruby that referenced this issue Sep 23, 2016
@headius headius Remove premade field-based DynamicScope and generate instead.
Instead of having hand-written DynamicScope subtypes that use
fields instead of an array, this logic generates those subclasses
as needed.

Currently it only overrides the base getValue and setValue methods
and DynamicScope now implements the other previously-abstract
methods by calling those two. This is not optimal for generic
access via those indexed methods, but the expectation is that JIT
can just go after the fields directly. The additional overrides
would likely be useful for the interpreter, though.

See #4167.
a8eece9
@headius headius added a commit to headius/jruby that referenced this issue Sep 23, 2016
@headius headius Remove premade field-based DynamicScope and generate instead.
Instead of having hand-written DynamicScope subtypes that use
fields instead of an array, this logic generates those subclasses
as needed.

Currently it only overrides the base getValue and setValue methods
and DynamicScope now implements the other previously-abstract
methods by calling those two. This is not optimal for generic
access via those indexed methods, but the expectation is that JIT
can just go after the fields directly. The additional overrides
would likely be useful for the interpreter, though.

See #4167.
bb41cc7
@headius headius added a commit to headius/jruby that referenced this issue Sep 23, 2016
@headius headius Remove premade field-based DynamicScope and generate instead.
Instead of having hand-written DynamicScope subtypes that use
fields instead of an array, this logic generates those subclasses
as needed.

Currently it only overrides the base getValue and setValue methods
and DynamicScope now implements the other previously-abstract
methods by calling those two. This is not optimal for generic
access via those indexed methods, but the expectation is that JIT
can just go after the fields directly. The additional overrides
would likely be useful for the interpreter, though.

See #4167.
c9370c2
@enebo
Member
enebo commented Nov 7, 2016

This is not addressed in full interpreter but I am closing this as resolved since we have it where it matters (JIT) and I want it in 9.1.6.0 issue list.

@enebo enebo closed this Nov 7, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment