block scope status, march edition
Hi, I have been plugging away at the block scope stuff. My current work is on https://bugs.webkit.org/show_bug.cgi?id=74708. Like some other patches, it uses catch clauses as the proof of concept, because there are so many tests around that area. It does lazy creation and tear-off if possible, and only in the case in which a scope has captured variables. I have two remaining things to fix before it is fully baked. One is static lookup of free variables (e.g. ResolveResult::Lexical variables). The issue is that with lazy scope creation ("reification") the depth in the scope chain cannot be computed statically. Let's say you are resolving "foo" in: (function () { var foo = 1; return function () { var bar = 3; function qux(x) { return bar+x; } return bar + foo; } })() Here, both functions will lazily create their activations. (I put in the qux to make sure the inner function has a lazy activation.) Here is their bytecode: Outer: 32 m_instructions; 256 bytes at 0xb10cf0; 1 parameter(s); 12 callee register(s); 4 variable(s) [ 0] enter [ 1] init_lazy_reg r0 [ 3] init_lazy_reg r2 [ 5] init_lazy_reg r1 [ 7] mov r3, Int32: 1(@k0) [ 10] create_activation r0 [ 12] new_func_exp r4, f0 [ 15] mov r5, Undefined(@k1) [ 18] call r4, 1, 12 [ 24] op_call_put_result r4 [ 27] tear_off_activation r0, r2 [ 30] ret r4 Constants: k0 = Int32: 1 k1 = Undefined Inner: 27 m_instructions; 216 bytes at 0xb0c930; 1 parameter(s); 6 callee register(s); 5 variable(s) [ 0] enter [ 1] init_lazy_reg r0 [ 3] init_lazy_reg r2 [ 5] init_lazy_reg r1 [ 7] init_lazy_reg r4 [ 9] mov r3, Int32: 3(@k0) [ 12] get_scoped_var r5, 3, 1 [ 17] add r5, r3, r5 [ 22] tear_off_activation r0, r2 [ 25] ret r5 Constants: k0 = Int32: 3 Note the get_scoped_var in the inner function, corresponding to the resolution of "foo". It indicates that "foo" can be found in the third register of the scope object one scope deep. However there might be an additional object on the scope chain, corresponding to the lazily created activation of the inner function. There is explicit code to account for skipping over an activation in the implementations of put_scoped_var and get_scoped_var. But with more lazily reified scopes that may or may not be on the scope chain (depending on whether the code path traversed a with, eval, or function expression), such an approach doesn't work. You can't determine the static depth from the top of the scope chain, because you'd like to avoid creating the entire scope chain if you can. I suggest that we change the users of scope chains (eval, with, and function expressions) to receive the scope chain register as a parameter. The compiler already has to handle lazy scope creation, so it's just as easy to put the scope in a temporary as it is to put it in the callframe. Free variable lookup will continue to use the register in the callframe. As a bonus, that code can be simpler because it doesn't have to check if the there is an activation or not. OK, that's one. The second issue is handling temporal dead zone errors. In ES6 it is a SyntaxError if you access the value of a let- or const-bound variable before it is initialized. In practice this means that in the general case, we mark uninitialized variables with a sentinel value (JSValue(), via init_lazy_reg). Access to let- or const-bound variables that need a barrier is preceded by a check that the variable is initialized. We can prove for some cases that no barrier is needed, and elide the barrier, but OK. My question is how to structure this check. Currently I have an assert_lazy_reg_init opcode that throws an error if the value isEmpty(). I guess we need to associate a name with that error too, so that opcode should take an identifier as a parameter as well. WDYT? Currently there is also an assert_lazy_reg_not_init opcode, but that was based on a misunderstanding -- you can only initialize const variables, not assign to them. Hence initialization doesn't need such a check. Finally, two more thoughts. One is that we can use op_reify_scope / op_tear_off_scope for parameters and locals in some cases, if a function does not have non-strict eval, as Gavin suggested. Also I hope to get a patch for "const" in today. The break-the-web aspect is a bit tricky, but we'll see how that goes. Thanks for comments! Andy -- http://wingolog.org/
Hi Andy, I had a conversation with Geoff the other day about the block scope work, and I thought of some useful formulations of performance constraints on the design. Folks have been telling you a lot about how to implement, I am hoping some of these rules can readily be turned into comparison benchmarks to test if a patch meets the goal. Here is the most basic goal: Constraint 1) Consider a function vf1() that contains no closures and uses var in a correctly block-scoped manner; ==> If you replace all use of var with let in function vf1() to create function lf1(), then lf1() should not suffer any speed or memory penalty compared to vf1() Constraint 1 implies that let-containing blocks should never create activations if the function does not contain closures, among other things, and you can turn whether you did this right into a test Now more advanced constraints: Now more advanced constraints: Constraint 3) Consider a function vf2() that uses var in a correctly block-scoped manner, and which contains a nested function expression, which may or may not be reached depending on parameters to vf2(): ==> If you replace all use of var with let in function vf2() to create function lf2(), then lf2() should not suffer any speed or memory penalty compared to vf2() *in the case where the closure is not actually created* This implies lazy creation of activations. Constraint 3) Consider a function vf3() that uses var in a correctly block-scoped manner, and which contains a closure that is reachable only once per call (i.e. not in a loop): ==> If you replace all use of var with let in function vf3() to create function lf3(), then lf3() should not suffer any speed or memory penalty compared to vf3() This implies merging function and block activations when possible. I think these constraints represent reasonable author expectations about use of let, especially constraint 1. You should be able to use it as just a "better var" without a performance penalty, unless you are using it in a way that imposes a relevant semantic difference. I bet the main JSC hackers could probably provide other similar constraint statements that express their performance goals for let in an unambiguous way. I suspect that it might be ok for the initial patch to only meet a subset of such constraints. Regards, Maciej On Mar 21, 2012, at 3:32 AM, Andy Wingo wrote:
Hi,
I have been plugging away at the block scope stuff. My current work is on https://bugs.webkit.org/show_bug.cgi?id=74708. Like some other patches, it uses catch clauses as the proof of concept, because there are so many tests around that area. It does lazy creation and tear-off if possible, and only in the case in which a scope has captured variables.
I have two remaining things to fix before it is fully baked.
One is static lookup of free variables (e.g. ResolveResult::Lexical variables). The issue is that with lazy scope creation ("reification") the depth in the scope chain cannot be computed statically. Let's say you are resolving "foo" in:
(function () { var foo = 1; return function () { var bar = 3; function qux(x) { return bar+x; } return bar + foo; } })()
Here, both functions will lazily create their activations. (I put in the qux to make sure the inner function has a lazy activation.) Here is their bytecode:
Outer: 32 m_instructions; 256 bytes at 0xb10cf0; 1 parameter(s); 12 callee register(s); 4 variable(s)
[ 0] enter [ 1] init_lazy_reg r0 [ 3] init_lazy_reg r2 [ 5] init_lazy_reg r1 [ 7] mov r3, Int32: 1(@k0) [ 10] create_activation r0 [ 12] new_func_exp r4, f0 [ 15] mov r5, Undefined(@k1) [ 18] call r4, 1, 12 [ 24] op_call_put_result r4 [ 27] tear_off_activation r0, r2 [ 30] ret r4
Constants: k0 = Int32: 1 k1 = Undefined
Inner: 27 m_instructions; 216 bytes at 0xb0c930; 1 parameter(s); 6 callee register(s); 5 variable(s)
[ 0] enter [ 1] init_lazy_reg r0 [ 3] init_lazy_reg r2 [ 5] init_lazy_reg r1 [ 7] init_lazy_reg r4 [ 9] mov r3, Int32: 3(@k0) [ 12] get_scoped_var r5, 3, 1 [ 17] add r5, r3, r5 [ 22] tear_off_activation r0, r2 [ 25] ret r5
Constants: k0 = Int32: 3
Note the get_scoped_var in the inner function, corresponding to the resolution of "foo". It indicates that "foo" can be found in the third register of the scope object one scope deep. However there might be an additional object on the scope chain, corresponding to the lazily created activation of the inner function.
There is explicit code to account for skipping over an activation in the implementations of put_scoped_var and get_scoped_var. But with more lazily reified scopes that may or may not be on the scope chain (depending on whether the code path traversed a with, eval, or function expression), such an approach doesn't work. You can't determine the static depth from the top of the scope chain, because you'd like to avoid creating the entire scope chain if you can.
I suggest that we change the users of scope chains (eval, with, and function expressions) to receive the scope chain register as a parameter. The compiler already has to handle lazy scope creation, so it's just as easy to put the scope in a temporary as it is to put it in the callframe.
Free variable lookup will continue to use the register in the callframe. As a bonus, that code can be simpler because it doesn't have to check if the there is an activation or not.
OK, that's one.
The second issue is handling temporal dead zone errors. In ES6 it is a SyntaxError if you access the value of a let- or const-bound variable before it is initialized. In practice this means that in the general case, we mark uninitialized variables with a sentinel value (JSValue(), via init_lazy_reg). Access to let- or const-bound variables that need a barrier is preceded by a check that the variable is initialized. We can prove for some cases that no barrier is needed, and elide the barrier, but OK.
My question is how to structure this check. Currently I have an assert_lazy_reg_init opcode that throws an error if the value isEmpty(). I guess we need to associate a name with that error too, so that opcode should take an identifier as a parameter as well. WDYT?
Currently there is also an assert_lazy_reg_not_init opcode, but that was based on a misunderstanding -- you can only initialize const variables, not assign to them. Hence initialization doesn't need such a check.
Finally, two more thoughts. One is that we can use op_reify_scope / op_tear_off_scope for parameters and locals in some cases, if a function does not have non-strict eval, as Gavin suggested. Also I hope to get a patch for "const" in today. The break-the-web aspect is a bit tricky, but we'll see how that goes.
Thanks for comments!
Andy -- http://wingolog.org/ _______________________________________________ squirrelfish-dev mailing list squirrelfish-dev@lists.webkit.org http://lists.webkit.org/mailman/listinfo.cgi/squirrelfish-dev
Hello Maciej! This is great feedback. I think the work I have done meets your points (1) and (2), modulo the TDZ, for which we need to add some analysis to elide the barrier. (3) is on my radar. We'll see once I get the fully-baked patch with let and const. The broader point about the need to turn these into tests is well-taken. Would it be useful to develop some sort of ES6 performance suite? Otherwise we run the risk of not noticing regressions. (Of course we need a test suite as well; dunno if test262 is appropriate, or if we need to build something specific.) Regards, Andy -- http://wingolog.org/
On Mar 21, 2012, at 1:48 PM, Andy Wingo wrote:
Hello Maciej!
This is great feedback. I think the work I have done meets your points (1) and (2), modulo the TDZ, for which we need to add some analysis to elide the barrier. (3) is on my radar. We'll see once I get the fully-baked patch with let and const.
The broader point about the need to turn these into tests is well-taken. Would it be useful to develop some sort of ES6 performance suite?
It would be good to have performance tests of ES6 (and ES5!) features, but probably beyond the scope of this specific task. I think one place where it is relevant to make comparison tests is when a new feature is intended to replace an old way of doing something. In that case, authors reasonably would expect no perf hit from using the new feature the same as the old one. Examples: - Native JSON replaces JS-based JSON impls, so it needs to perform as well or better. - Function.bind() replaces hand-rolled bind implementations, so it needs to perform as well or better. - let replaces var, so it needs to perform as well or better. There may be other such examples. For features that have no old equivalent, there isn't an equally obvious minimum performance bar. Regards, Maciej
Hi, A small update here: On Wed 21 Mar 2012 11:32, Andy Wingo <wingo@igalia.com> writes:
I suggest that we change the users of scope chains (eval, with, and function expressions) to receive the scope chain register as a parameter. The compiler already has to handle lazy scope creation, so it's just as easy to put the scope in a temporary as it is to put it in the callframe.
This doesn't work because we need to be able to tear off block scopes on an exceptional exit. So my current strategy will be to add a flag to codeblocks that push items onto the scope chain, hasLocalScopeChain() or so. We'll reserve a register to hold the local scope chain, also stored in the codeblock.
The second issue is handling temporal dead zone errors.
I renamed assert_lazy_reg_init to ensure_local_initialized, following the existing ensure_property_exists. Errors are now printed more nicely. Block-scoped locals are only initialized via init_lazy_reg if they need the temporal dead zone. So the current status is that I'm trying to get the runtime to know what scope objects are pushed locally to an Executable*, and which are "outer". This is both for variable access and for exceptional unwinding. Hackety hack, Andy -- http://wingolog.org/
On Mar 26, 2012, at 9:01 AM, Andy Wingo wrote:
This doesn't work because we need to be able to tear off block scopes on an exceptional exit. So my current strategy will be to add a flag to codeblocks that push items onto the scope chain, hasLocalScopeChain() or so. We'll reserve a register to hold the local scope chain, also stored in the codeblock.
Sounds sensible. It might help to keep this at a fixed location, save having to look up the register number every time. We may be able to ensure this is always the first register allocated, if needed, & always in local 0? Just a thought. G.
On Mar 26, 2012, at 12:24 PM, Gavin Barraclough wrote:
On Mar 26, 2012, at 9:01 AM, Andy Wingo wrote:
This doesn't work because we need to be able to tear off block scopes on an exceptional exit. So my current strategy will be to add a flag to codeblocks that push items onto the scope chain, hasLocalScopeChain() or so. We'll reserve a register to hold the local scope chain, also stored in the codeblock.
Sounds sensible. It might help to keep this at a fixed location, save having to look up the register number every time. We may be able to ensure this is always the first register allocated, if needed, & always in local 0? Just a thought.
We already make specific assumptions about the location of the arguments and activation registers being stored in early registers, we just need to store which index is being used in the same way. No need for a specific constant location. --Oliver
G.
_______________________________________________ squirrelfish-dev mailing list squirrelfish-dev@lists.webkit.org http://lists.webkit.org/mailman/listinfo.cgi/squirrelfish-dev
participants (4)
-
Andy Wingo
-
Gavin Barraclough
-
Maciej Stachowiak
-
Oliver Hunt