-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
let support for ES3/ES5 #1690
Comments
We're not planning on it at the moment. There are some cases like loop variables where the rewriting necessary is very invasive in terms of performance overhead, e.g. in a loop you need to wrap the loop body in a function, which could easily be a 10x perf hit: for(let i = 0; i < 10; i++) {
window.setTimeout(() => {
console.log(i)
}, 10);
} The proper emit for this would be: for (var i = 0; i < 10; i++) {
(function (i) {
window.setTimeout(function () {
console.log(i);
}, 10);
})(i);
} |
Probably just me but I don't see it: var count = 1000;
console.time('var');
for (var i = 1; i <= count; i++) {
var varCallCount = 1;
window.setTimeout(function () {
if(varCallCount++ == count) console.timeEnd('var');
}, 10);
}
console.time('let');
for (var i = 1; i <= count; i++) {
var letCallCount = 1;
(function (i) {
window.setTimeout(function () {
if(letCallCount++ == count) console.timeEnd('let');
}, 10);
})(i);
} My results:
|
@basarat The code you posted doesn't use I came up with the following code, that creates a closure on the loop variable in 3 different way: using ES6 let, using an immediate function (what 6to5 does), using try-catch (what Traceur does): 'use strict';
var count = 10000;
var callback;
console.time('let');
for (let i = 0; i < count; i++) {
callback = function() { return i; };
}
console.timeEnd('let');
console.time('function');
for (var j = 0; j < count; j++) {
(function (_j) {
callback = function() { return _j; };
})(j);
}
console.timeEnd('function');
console.time('try-catch');
for (var k = 0; k < count; k++) {
try { throw k; }
catch (_k) {
callback = function() { return _k; };
}
}
console.timeEnd('try-catch'); Results on my machine, IE11: Note: IE11 doesn't handle the latest The immediate function might be OK, especially if you only inject it wherever code analysis shows that it's required. The traceur approach (try-catch) is really much slower. |
Wrapping loop body in IIFE can become a reason of interesting memory issues because of closure creation. An example for V8 can be found here. If runtime supports |
I modified your tests so that your array isn't sparse. Basically I used I see what you tried to do. Because I don't know why I can't see the leak, maybe the V8 optimized further since 2012? The optimizer could notice that IIFE doesn't escape the function scope and hence doesn't need any closure at all. Or they may notice that the result only needs the IIFE context, not createWithVar's context. In any case I don't see the leak. Is this a motivation enough to hold off implementation? Anyway if I am required to code this, I will use the IIFE technique by hand or use 6to5, which will do the same... On the other hand, looking forward, I think that the IIFE transform may have complicated interactions with other features, such as async and generators? For instance, playing with 6to5 REPL, the following code doesn't generate correct ES5 code: async function test(y) {
var result = [];
for (let i = 0; i < 4; i++) {
await y;
result.push(() => i);
}
return result;
} |
Just tried Chrome 42.0.2279.2 (Official Build) canary (64-bit), V8 4.2.10 |
Yes, I don't know what I missed before. |
Reopening based on feedback |
Babel actually handles cases with loops fairly well at this time. It has some quirks where perf could probably be better (though the ones I found a while back has all been fixed). The following for instance, does not produce any IIFE: // original
for(let i = 0; i < 10; i++) {
console.log(`1: ${i}`);
for (let i = 0; i < 10; i++) {
console.log(`2: ${i}`);
}
}
// transpiled
for (var i = 0; i < 10; i++) {
console.log("1: " + i);
for (var _i = 0; _i < 10; _i++) {
console.log("2: " + _i);
}
} However, if I do things that requires // original
let arr = [];
for(let i = 0; i < 10; i++) {
arr.push(() => i);
}
// transpiled
var arr = [];
for (var i = 0; i < 10; i++) {
(function (i) {
arr.push(function () {
return i;
});
})(i);
} I've seen that in most of the cases where I use // original
let arr = [];
for(let i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
arr.push(() => i);
}
}
// transpiled
var arr = [];
for (var i = 0; i < 10; i++) {
(function (i) {
for (var _i = 0; _i < 10; _i++) {
(function (_i) {
arr.push(function () {
return _i;
});
})(_i);
}
})(i);
} As far as I can tell, it shouldn't need to create an IIFE on the outer loop here, but that might just be me thinking about things wrong. |
Babel was improved recently and is quite good at transpiling async function test() {
for (let i in [0,1,2]) {
await y(() => i);
}
} It is also relatively clever when it decides to create the IIFE. For instance, in this case it needs a function to capture the scope of // This code:
for (var a = 0; a++; a < 10) {
let b = a + 1;
y(() => a * b);
}
// Generates:
for (var a = 0; a++; a < 10) {
(function () {
var b = a + 1;
y(function () {
return a * b;
});
})();
} But if I change // This code:
for (let a = 0; a++; a < 10) {
let b = a + 1;
y(() => a * b);
}
// Generates:
for (var a = 0; a++; a < 10) {
(function (a) {
var b = a + 1;
y(function () {
return a * b;
});
})(a);
} Multiple // This code:
for (let a = 0, b = 2; a++, b--; b > 0) {
y(() => a * b);
}
// Generates:
for (var a = 0, b = 2; a++, b--; b > 0) {
(function (a, b) {
y(function () {
return a * b;
});
})(a, b);
} There is a lot of work and special cases in this feature... It makes me wonder: can't you reuse babel code? Babel transformers use the de-facto standard AST that was created by Mozilla. I don't know which one TypeScript uses... |
@jods TypeScript does not use Mozilla's syntax tree model. It uses one more suitable for use in the IDE scenarios that TS cares a lot about. The AST we use is deisgned to work extremely well in the presence of broken code (the normal case in an IDE). It is also designed to support extremely fast incremental updates, allowing edits to be incorporated orders of magnitude faster than normal parsing. Finally, it is designed to have a very small memory footprint, eschewing large values for ints, and storing redundant data on the prototype instead of on the instance. |
@CyrusNajmabadi To be honest, I was expecting that! Well, maybe you can take inspiration from the babeljs logic anyway, or even just their test cases. E.g. goto labels var stack = [];
loop1:
for (let j = 0; j < 10; j++) {
loop2:
for (let i = 0; i < 10; i++) {
for (let x = 0; x < 10; x++) {
stack.push(() => [j, i, x]);
if (stack.length > 5) continue loop2;
stack.push(() => [j, i , x]);
}
}
} Or return statements (function () {
for (let i in nums) {
fns.push(function () { return i; });
return;
}
})(); |
@Alxandr Allowing "let" to always define a self calling function hurts performance too much. See your sample here: http://jsperf.com/let-by-function-in-loop |
@jlennox Yes. But |
There's also the option for the compiler to just error out or warn if it fears performance will suffer. Just like jslint's "don't create functions in loops" - it's generally not something you should do when you care about performance. It'd be great if we had this in ts1.5! |
@hdachev jshint actually tells you to create IIFEs though ^^ http://jshint.com/docs/options/#loopfunc It's not about perf, it's about the fact that the variables aren't bound to the value, but the function scoped variable, so you get unexpected behavior. |
Ha! Dunno why I never thought about that, it's kinda obvious. In any case, my point was that the compiler could output a warning when it has performance concerns about the code it's emitting (not saying I think the occasional IIFE in a loop can realistically become a bottleneck in anyone's application). Much better than not having es5 emit at all. |
Assuming that IIFE are only emitted when really required, which is inside loop closures. Forget perf for a second: what would you replace them with anyway? for (let i in [0,1,2])
y(() => i); Assume you need to create a piece of code that performs exactly what the piece of code above does. What will you do in ES5? In those cases I use a IIFE anyway, do you have a better solution? If you have no better solution, then perf doesn't matter at all. IIFE is what will be used to achieve this functionnality in a ES5 browser. Transpiled or coded by hand (I prefer transpiled). |
@jods4 "If you have no better solution, then perf doesn't matter at all" The goal with downlevel emit isn't to support 100% of all use cases people may want to use ES6 for. We want to hit a sweet spot where we support the majority of use cases that people care about most of the time, while also not introducing a large amount of complexity into our compiler. For example, if doing this helped out a handful of customers, in scenarios where they could just workaround the issue themselves, but it added 50k lines of code, then it probably would not be worth it. Now, if it turns out this is a really popular feature that many customers are requesting, then that will change our evaluation of how worthwhile it is to do. |
@Alxandr You'll note that TS handles this case as well. The current TS compiler will produce: for (var i = 0; i < 10; i++) {
console.log("1: " + i);
for (var _i = 0; _i < 10; _i++) {
console.log("2: " + _i);
}
} |
@CyrusNajmabadi I don't see this problem as a "ES6 feature". I think of it as a coding requirement. Sometimes you have to create a lambda for each element of a loop iteration (maybe an array). And I don't know any better way to achieve that in ES5 than using an IIFE in a loop. That's what I've always done, even before ES6 was "available". But this practice is ugly, not as performant as it could be and may unwillingly capture huge amounts of memory. ES6 finally brings a perfect fix to this issue, by introducing a way to scope a variable to the loop body. That's a huge win for anyone who has to write such code. Now I understand your argument about complexity. We can always do TS -> Babel -> ES5 anyway. |
@Alxandr You must of misunderstood the purpose of the test. The top case was to demonstrate the expected performance, the second to demonstrate what the actual compiled code performance would of been. They both result in arr == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. The compiler may be able to work around using an inline function sometimes, and you could make it give a warning when there would be performance degradation, but this is an unsolvable problem.
|
It's possible to get a huge increase in performance by using a temporary function declared after the loop, like this: for (var i = 0; i < 100; i++) _a(i);
function _a(i) {
// for body
} |
@jlennox yeah, with the suggestion of @ivogabe you're down to a 33% perf reduction (http://jsperf.com/let-by-function-in-loop/2). Also, the thing to note though, is that the only other way to achieve the same result, is by writing that code by hand. Which would in turn make perf worse when we get to a point where browsers understand it, because then you'd have hand-written code that calls functions instead of simply looping. |
This is now checked in. |
Any plan for ES3/ES5 support similar to this
The text was updated successfully, but these errors were encountered: