Traits library

Brendan Eich brendan at mozilla.com
Thu Feb 18 09:32:18 PST 2010


On Feb 17, 2010, at 9:29 PM, Brendan Eich wrote:

> Bound methods need not be reified unless extracted as funargs (even  
> if only as operands of ===, e.g.), or frozen unless tampered with.  
> If they're (typically, or only ever) called on the right receiver,  
> then the cost of creating a bind result can be avoided.
>
> Optimizing to defer layered freeze/bind combinations until method  
> value extraction or mutation attempt requires more than fancy  
> pattern-matching on the part of an implementation. It requires a  
> read barrier.

In case it wasn't clear, a read barrier is enough, even to cope with  
mutation attempts (deferred freeze), since method extraction is a  
read, and to mutate a method value (function object) you have to read  
(not invoke) the method value:

o.m();       // ok, invoking with bound receiver, not reading o.m
foo(o.m);    // not ok, must bind and freeze o.m
o.m.bar = 1; // mutation on o.m reference base, first bind and freeze  
o.m

The barrier can thus be compiled into forms that read o.m without  
immediately invoking it. Of course the overhead is still too high if  
we only compile it, so there has to be a runtime fast path (a  
polymorphic inline cache), but that is already the case due to  
getters, as noted.

If the method body uses arguments and is not strict-mode code, then it  
could somehow use arguments.callee and leak an unbound, unfrozen (and  
"joined", see ES3 -- the first optimization to do here, prior to  
traits and anything in ES5, is to defer evaluating a function  
expression into a fresh object per evaluation -- SpiderMonkey does  
this optimization under some conditions) method reference.

So arguments is a hazard (it's hard to analyze precisely for  
arguments.callee, if you see arguments[i] or baz(arguments) you have  
to assume the worst).

Same goes for named function expressions as methods, where the method  
body or a function nested in it uses the name of the function  
expression.

These are more places that need the barrier, or (to simplify  
compilation) defeat the optimization of deferring bind and freeze (and  
disjoining).

But let's back up again, to traits.js:

var Trait = (function(){
  . . .
   var SUPPORTS_DEFINEPROP = !!Object.defineProperty;

   var call = Function.prototype.call;
  . . .

Static analysis of just this file cannot be sure that Object and  
Function have their original meanings -- these are writable properties  
of the global object. So some speculation, at the least, is required  
to get off the ground at all, in trying to pattern-match  
freeze(bind(...)) etc.

Recognizing when Object and Function still mean what they originally  
meant is something optimizing VMs may do, but it's a high bar to set  
for traits performance to scale well.

The method read barrier complexity on top is also on the boards or  
already partly implemented in at least one VM, but again why should  
this be required of implementors, on top of the syn-tax on users?

/be


More information about the es-discuss mailing list