Discussion:
[jruby-dev] Eliminating block-as-binding as a potential deoptimizer
(too old to reply)
Charles Oliver Nutter
2009-07-25 14:56:56 UTC
Permalink
In Ruby, you can use a block as though it were a binding:

def foo(&b)
eval 'puts a', b
end

def bar
a = 1
foo {} # prints out '1'
end

It is largely because of this feature that our block scoping is very
costly; whenever a block is present, the containing method's local
variables must all be lifted to the heap, so they can be accessed
across calls. This means that 'bar' above will have a permanent
performance penalty for all local variables, even though none of them
are accessed within the block passed to 'foo'.

In Groovy, there's no ability to use a block as a binding, so they
only close over the variables that will actually be accessed. This
means that you only pay the heap-scoping cost for variables you
explicitly use across the closure boundary. This allows Groovy's
closures to be considerably lighter-weight, and in many cases be as
fast as any other call.

But there is a way out of this. Yehuda and I talked a bit about it and
realized that if we could inspect the target method whenever we're
calling a block, we could determine whether a full heap scope would be
needed. There are only three constructs that would trigger this
potential:

* A block argument (&arg) in the parameter list, because it's
capturing the block for later use
* A zsuper call (no-arg, no-parens super), because that call could
capture the block for later use
* If the call itself is Proc.new, proc, or lambda, because it's
capturing the block (or -> in 1.9)
* Any eval-like call (binding, eval)

All three of these are inspectable at method-construction time, so we
could have a flag on method objects for "might do evil things with
your block". At that point, we could lazily lift all our local
variables to a heap structure and follow the heap-scoping path from
then on, This would have to be a hard trap + branch to deoptimized
code, but it could work. We will need to think about how to deoptimize
by branching to a new body of codewith current execution state, but I
think it's possible.

An alternative to dynamic deopt would be to add profiling information
at the call site indicating whether we ever see a block used as a
binding. We would still need to consider a deopt mechanism, but we
would not necessarily need to depend on it as much, due to the
profiling probably being all the information we need.

I would strongly prefer this feature just disappear, but it's not going to.

- Charlie

---------------------------------------------------------------------
To unsubscribe from this list, please visit:

http://xircles.codehaus.org/manage_email
Subramanya Sastry
2009-07-25 19:29:45 UTC
Permalink
But there is a way out of this. Yehuda and I talked a bit about it and
Post by Charles Oliver Nutter
realized that if we could inspect the target method whenever we're
calling a block, we could determine whether a full heap scope would be
needed. There are only three constructs that would trigger this
* A block argument (&arg) in the parameter list, because it's
capturing the block for later use
* A zsuper call (no-arg, no-parens super), because that call could
capture the block for later use
* If the call itself is Proc.new, proc, or lambda, because it's
capturing the block (or -> in 1.9)
* Any eval-like call (binding, eval)
Also send with a target that is not resolveable at compile time + any
aliased method calls that might resolve to an eval or a send.

-S.
Yehuda Katz
2009-07-25 22:28:55 UTC
Permalink
Post by Charles Oliver Nutter
But there is a way out of this. Yehuda and I talked a bit about it and
Post by Charles Oliver Nutter
realized that if we could inspect the target method whenever we're
calling a block, we could determine whether a full heap scope would be
needed. There are only three constructs that would trigger this
* A block argument (&arg) in the parameter list, because it's
capturing the block for later use
* A zsuper call (no-arg, no-parens super), because that call could
capture the block for later use
* If the call itself is Proc.new, proc, or lambda, because it's
capturing the block (or -> in 1.9)
* Any eval-like call (binding, eval)
Also send with a target that is not resolveable at compile time + any
aliased method calls that might resolve to an eval or a send.
I think it would be ok to flush all of the bytecode when send was aliased
(which never happens). We could consider send with an unresolvable target to
be "dangerous". The key is that dangerous methods are not all that
expensive; they simply check a flag to check whether they need to retrieve
backref info etc., so if they don't have to, the additional cost is just a
simple boolean check (vs. the cost of instantiating a frame for EVERY call).
Post by Charles Oliver Nutter
-S.
--
Yehuda Katz
Developer | Engine Yard
(ph) 718.877.1325
Charles Oliver Nutter
2009-07-26 00:44:52 UTC
Permalink
Post by Yehuda Katz
I think it would be ok to flush all of the bytecode when send was aliased
(which never happens). We could consider send with an unresolvable target to
be "dangerous". The key is that dangerous methods are not all that
expensive; they simply check a flag to check whether they need to retrieve
backref info etc., so if they don't have to, the additional cost is just a
simple boolean check (vs. the cost of instantiating a frame for EVERY call).
Flushing bytecode is not sufficient for OSR, but we may be able to
mitigate or ignore that fact, since most such flushes will occur long
before we have made final decisions on optimizations.

A tiered compiler that eventually settles into a "perfect view" of the
system may be reasonable, if we can urge people toward doing these
damaging calls much earlier.

- Charlie

---------------------------------------------------------------------
To unsubscribe from this list, please visit:

http://xircles.codehaus.org/manage_email

Loading...