Почему использование `let` внутри цикла` for` настолько медленное в Chrome?

ОСНОВНОЕ ОБНОВЛЕНИЕ.

Мысль о том, что еще не выпущена в Chrome, выпущена новая Ignition + Turbofan engine для Chrome Canary 59, решила проблему. Тест показывает идентичные времена для let и var объявленных переменных цикла.


Оригинальный (теперь немой) вопрос.

При использовании let в цикле for в Chrome он работает очень медленно, по сравнению с перемещением переменной непосредственно за пределы области цикла.

for(let i = 0; i < 1e6; i ++); 

занимает в два раза длиннее

{ let i; for(i = 0; i < 1e6; i ++);}

Что происходит?

Фрагмент демонстрирует разницу и влияет только на Chrome, и так было до тех пор, пока я помню Chrome, поддерживающий let.

var times = [0,0]; // hold total times
var count = 0;  // number of tests

function test(){
    var start = performance.now();
    for(let i = 0; i < 1e6; i += 1){};
    times[0] += performance.now()-start;
    setTimeout(test1,10)
}
function test1(){
    // this function is twice as quick as test on chrome
    var start = performance.now();
    {let i ; for(i = 0; i < 1e6; i += 1);}
    times[1] += performance.now()-start;
    setTimeout(test2,10)
}

// display results
function test2(){
    var tot =times[0]+times[1];
    time.textContent = tot.toFixed(3)  + "ms";
    time1.textContent = ((times[0]/tot)*100).toFixed(2) + "% " + times[0].toFixed(3)  + "ms";
    time2.textContent = ((times[1]/tot)*100).toFixed(2) + "% " + times[1].toFixed(3) + "ms";
    if(count++ < 1000){;
        setTimeout(test,10);
    }
}
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
var time = document.createElement("span");
var time1 = document.createElement("span");
var time2 = document.createElement("span");
div.textContent = "Total execution time : "
div1.textContent = "Test 1 : "
div2.textContent = "Test 2 : "
div.appendChild(time);
div1.appendChild(time1);
div2.appendChild(time2);
document.body.appendChild(div);
document.body.appendChild(div1);
document.body.appendChild(div2);
test2()

Ответы

Ответ 1

ОСНОВНОЕ ОБНОВЛЕНИЕ.

Мысль о том, что еще не выпущена в Chrome, выпущена новая Ignition + Turbofan engine для Chrome Canary 60.0.3087, решила проблему. Тест показывает идентичные времена для let и var объявленных переменных цикла.

Боковое примечание. В моем тестовом коде используется Function.toString() и не удалось на Canary, потому что он возвращает "function() {" not "function () {" в качестве прошлых версий (легко исправить с помощью regexp), но потенциальная проблема для тех, кто используйте Function.toSting()

Обновление Благодаря пользователю Dan. M, которые предоставляют ссылку https://bugs.chromium.org/p/v8/issues/detail?id=4762 (и голова вверх), у которой больше проблем.


Предыдущий ответ

Оптимизатор отказался.

Этот вопрос озадачил меня в течение некоторого времени, и два ответа являются очевидными ответами, но это не имело никакого смысла, поскольку разница во времени была слишком велика, чтобы создать новую область действия и контекст выполнения.

Чтобы доказать это, я нашел ответ.

Короткий ответ

А для цикла с выражением let в объявлении не поддерживается оптимизатором.

Профиль Chrome двух функций, показывающих, что медленная функция не оптимизирована Chrome Version 55.0.2883.35 beta, Windows 10.

Изображение стоит тысяча слов, и должно быть, первое место, чтобы посмотреть.

Соответствующие функции для вышеуказанного профиля

var time = [0,0]; // hold total times

function letInside(){
    var start = performance.now();

    for(let i = 0; i < 1e5; i += 1); // <- if you try this at home don't forget the ;

    time[0] += performance.now()-start;
    setTimeout(letOutside,10);
}

function letOutside(){ // this function is twice as quick as test on chrome
    var start = performance.now();

    {let i; for(i = 0; i < 1e5; i += 1)}

    time[1] += performance.now()-start;
    setTimeout(displayResults,10);
}

Поскольку Chrome является основным игроком, а заблокированные переменные области для счетчиков циклов повсюду, те, кому нужен код исполнения, и считают, что переменные с узкоспециализированными важными важными function{}(for(let i; i<2;i++}{...})//?WHY? должны рассматривать на данный момент альтернативный синтаксис и объявлять счетчик циклов вне цикла.

Я хотел бы сказать, что разница во времени тривиальна, но в свете того, что весь код внутри функции не оптимизирован с использованием for(let i..., следует использовать с осторожностью.


Ответ 2

Поскольку для каждой итерации цикла создается новый i, так что замыкания, созданные в цикле, закрываются над i для этой итерации. Это объясняется спецификацией в алгоритме оценки тела цикла for, в котором описывается создание новой переменной для каждой итерации цикла.

Пример:

for (let i = 0; i < 5; ++i) {
  setTimeout(function() {
    console.log("i = " + i);
  }, i * 50);
}

// vs.
setTimeout(function() {
  let j;
  for (j = 0; j < 5; ++j) {
    setTimeout(function() {
      console.log("j = " + j);
    }, j * 50);
  }
}, 400);

Ответ 3

@T.J.Crowder уже ответил на заголовок вопроса, но я отвечу на ваши сомнения.

Когда я впервые столкнулся с этим, я подумал, что это из-за только что созданного экземпляра i, но следующее показывает, что это не так.

Собственно, это из-за недавно созданной области для переменной i. Что еще не оптимизировано, поскольку оно более сложное, чем простая область блока.

Смотрите второй фрагмент кода, поскольку я исключил возможность дополнительной оптимизации объявления объявления с помощью ini со случайным и затем добавлением к неопределенному значению k.

Ваше дополнительное объявление let j в

{let i; for (i = 0; i < 1e3; i ++) {let j = Math.random(); j += i; k += j;}}
// I'll ignore the `p` variable you had in your code

был оптимизирован. Это довольно тривиальная задача для оптимизатора, она может вообще избежать этой переменной, упростив тело цикла до

k += Math.random() + i;

Область действительно не нужна, если вы не создаете там закрытие или не используете eval или подобные мерзости.

Если ввести такое замыкание (как мертвый код, надеюсь, оптимизатор этого не осознает) и ямы

{let i; for (i=0; i < 1e3; i++) { let j=Math.random(); k += j+i; function f() { j; }}}

против

for (let i=0; i < 1e3; i++) { let j=Math.random(); k += j+i; function f() { j; }}

то мы увидим, что они работают примерно с той же скоростью.

var times = [0,0]; // hold total times
var count = 0;  // number of tests
var soak = 0; // to stop optimizations
function test1(){
    var k = time[1];
    var start = performance.now();
    {let i; for(i=0; i < 1e3; i++){ let j=Math.random(); k += j+i; function f() { j; }}}
    times[0] += performance.now()-start;
    soak += k;
    setTimeout(test2,10)
}
function test2(){
    var k = time[1];
    var start = performance.now();
    for(let i=0; i < 1e3; i++){ let j=Math.random(); k += j+i; function f() { j; }}
    times[1] += performance.now()-start;
    soak += k;
    setTimeout(display,10)
}

// display results
function display(){
    var tot =times[0]+times[1];
    time.textContent = tot.toFixed(3)  + "ms";
    time1.textContent = ((times[0]/tot)*100).toFixed(2) + "% " + times[0].toFixed(3)  + "ms";
    time2.textContent = ((times[1]/tot)*100).toFixed(2) + "% " + times[1].toFixed(3) + "ms";
    if(count++ < 1000){
        setTimeout(test1,10);
    }
}
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
var time = document.createElement("span");
var time1 = document.createElement("span");
var time2 = document.createElement("span");
div.textContent = "Total execution time : "
div1.textContent = "Test 1 : "
div2.textContent = "Test 2 : "
div.appendChild(time);
div1.appendChild(time1);
div2.appendChild(time2);
document.body.appendChild(div);
document.body.appendChild(div1);
document.body.appendChild(div2);
display();