This fixes #1005.
Here's a reproduction that I'll use to discuss the problem:
So the issue here is how the @@ variables are accessed.
@@var class vars are loaded using the current static scope (lexical scope from
parser) which contains a reference to the surrounding class at the time the
code was instantiated. The scope itself gets stored either in the AST
(interpreter) or in the AbstractScript associated with the compiled code. When
retrieving an class variable, we first acquire a reference to the scope and
then look up the variable against the class it references.
In the non-indy compiler, the scope is retrieved each time from the
AbstractScript object associated with the currently executing code. This
requires hopping through a couple levels of indirection, but if the same JVM
bytecode is used with different logical code bodies, they continue to work
properly since they always go to the AbstractScript object.
The situation with indy is a bit different.
In order to make it possible for the JVM JIT to fold away repeated accesses of
the same scope, we permanently cache the scope at the physical call site. The
call site is unique to a given loaded piece of bytecode, so if the same loaded
bytecode gets shared across logically different code bodies, we can end up
using the scope associated with a previously jitted version of the same code.
That's what's happening here.
In my example, the class variable accessors for @@bar are eval'ed into multiple
classes, but to our JIT cache they look identical since they don't include
anything to make them appear unique for each class. This means we use a single
loaded version of the bytecode and all subclasses end up sharing the same
bytecode. This means, in turn, they share a single call site...and the hard
caching of the scope ends up going to the first method to JIT. In this case,
that means FooAlpha's bar and @@bar are used rather than FooBeta's when the
latter's bar methods JIT.
The core problem here is the caching for JVM bytecode/class for a JITed method
does not have a sufficiently unique key to represent identical bytecode that
has different runtime artifacts attached to it...like static scope.
There are a few ways to fix this:
Modify the static scope logic in our invokedynamic backend to not permanently
cache the scope. This would work, but would cause repeat accesses of that scope
to go through full indirection every time. It may require also fixing other
values looked up from scope like constants...and losing JIT value folding for
constants would be a terrible step backward.
Modify the key for the JIT cache to include additional information about the
lexical scoping structures being used. This may require some help from the
parser, since at the moment there's no way to distinguish two StaticScope
objects other than by their hashcodes. Adding the scope hashcodes to the key
would be a quick way to fix this without completely breaking caching, but it
may be that bytecode caching in the presence of invokedynamic will bite us in
other ways down the road.
Disable JIT caching in the presence of invokedynamic. The feature was
originally added when it was assumed that all users would always need to run
multiple copies of the same JRuby application in a given JVM, where we would
potentially crumble under the load of all those mostly-identical JITed methods.
However, applications have been trending toward a single JRuby instance across
all threads, memory has been getting cheaper, and Java 8 eliminates the permgen
we were originally so concerned about.
I'm leaning strongly enough toward the third option that I'm going to go with
it for now. So many of our uses of invokedynamic require the loaded code and
call sites to be as unique as the logical methods they represent, and I can
think of several other places where the current caching could (and maybe
already is) biting us. This will potentially mean JRuby apps running under indy
load more code, but in actuality it will almost exclusively affect
multi-instance applications. Provide the JVM with more memory or run a single