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

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

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


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

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

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

занимает вдвое дольше, чем

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

Что здесь происходит?

Snippet демонстрирует разницу и влияет только на 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()

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

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

Я также добавил второй счетчик цикла p

var times = [0,0]; // hold total times
var count = 0;  // number of tests
var soak = 0; // to stop optimizations
function test(){
    var j;
    var k = time[1];
    var start = performance.now();
    for(let p =0, i = 0; i+p < 1e3; p++,i ++){j=Math.random(); j += i; k += j;};
    times[0] += performance.now()-start;
    soak += k;
    setTimeout(test1,10)
}
function test1(){
    // this function is twice as quick as test on chrome
    var k = time[1];
    var start = performance.now();
    {let p,i ; for(p = 0,i = 0; i+p < 1e3; p++, i ++){let j = Math.random(); j += i; k += j}}
    times[1] += performance.now()-start;
    soak += k;
    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()

3 ответа

Решение

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

Мысль о выпуске Chrome Canary 60.0.3087, которая еще не была выпущена на главном выпуске Chrome, решила эту проблему. Тестовое шоу идентичное время для letа такжеvar объявленные переменные цикла.

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

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


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

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

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

В попытке доказать это я нашел ответ.

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

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

Chrome профиль двух функций, показанных медленная функция не оптимизированаChrome Версия 55.0.2883.35 бета, 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... следует использовать с осторожностью.


Обновление: июнь 2018 года. Chrome теперь оптимизирует это гораздо лучше, чем когда первый раз были опубликованы этот вопрос и ответ; больше нет заметного штрафа за использование let в for если вы не создаете функции в цикле (и если вы это делаете, выгоды стоят затрат).


Потому что новый 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);

Это больше работы. Если вам не нужен новый i для каждого цикла используйте let вне петли. Смотрите обновление выше, не нужно избегать его, кроме крайних случаев.

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

Здорово, что другие движки уже провели оптимизацию, но команда V8, видимо, просто еще не достигла этого. Смотрите обновление выше.

@TJCrowder уже ответил на заглавный вопрос, но я отвечу на ваши сомнения.

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

На самом деле, это из-за недавно созданной области для i переменная. Который (пока) не оптимизирован, так как он более сложен, чем простая область видимости блока.

Посмотрите второй фрагмент кода, так как я исключил любую возможность оптимизации дополнительного объявления let с помощью 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();

Другие вопросы по тегам