Неожиданное замедление в Chrome при большом количестве итераций - PullRequest
4 голосов
/ 05 февраля 2020

Я сравнивал производительность двух функций в консоли Chrome, когда наткнулся на то, что не могу объяснить.

const A = 3,
    B = 2,
    C = 4,
    D = 2;

const mathCompare = (a1, a2, b1, b2) => {
    return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};

const logicCompare = (a1, a2, b1, b2) => {
    return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};

const runLooped = (j) => {
    for(let i = 0; i < 4; i++) {
        jsCompare(mathCompare, logicCompare, j);
    }
}

const jsCompare = (f1, f2, iterations) => {
    let a = jsPerf(f1, iterations);
    let b = jsPerf(f2, iterations);
    console.warn( iterations + " iterations:\n" + f1.name + ": " + a + " ms\n" + f2.name + ": " + b + " ms\n" + "delta: " + (a-b) + " ms");
}

const jsPerf = (f, iterations) => {
    let start = performance.now();

    for(let i = 0; i < iterations; i++) {
        f(A, B, C, D);
    }
    return performance.now() - start;
}

runLooped(10000000);
Запуск наборов 10M итераций n раз:
  • logicCompare () : все наборы take ~ 170ms
  • mathCompare () : первый набор занимает ~ 14ms , следующие наборы занимают ~ 600ms .

Поскольку производительность изменилась только после полного набора итераций - одного вызова jsCompare () - я решил попробовать еще раз с менее расформированной структурой:

const A = 3,
    B = 2,
    C = 4,
    D = 2;

const mathCompare = (a1, a2, b1, b2) => {
    return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};

const logicCompare = (a1, a2, b1, b2) => {
    return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};

const compareRaw = (f1, f2, maxI, maxJ) => {
    for(let i = 0; i < maxI; i++) {
        let j,
            a = performance.now();
        for(j = 0; j < maxJ; j++) {
            f1(A, B, C, D);
        }
        let b = performance.now();
        for(j = 0; j < maxJ; j++) {
            f2(A, B, C, D);
        }
        let c = performance.now();
        console.warn( j + " iterations:\n" + f1.name + ": " + (b-a) + " ms\n" + f2.name + ": " + (c-b) + " ms\n" + "delta: " + ((b-a) - (c-b)) + " ms");
    }
};

const runRaw = (i) => {
    compareRaw(mathCompare, logicCompare, 4, i);
};

runRaw(10000000);

Совсем другой результат. Результаты стабилизируются в районе 3, после некоторых колебаний.

  1. Chrome 79.0.3945.130:

    • logicCompare () : все наборы занимают ~ 12 мс
    • mathCompare () : все наборы занимают ~ 10 мс
  2. Vivaldi 2.6.1566.49 (V8 7.5.288.30 ):

    • logicCompare () : все наборы занимают ~ 60 мс
    • mathCompare () : все наборы возьми ~ 10 мс

Я был заинтригован и попробовал все снова, но на этот раз со случайными числами. Проект, для которого я тестировал этот проект, очевидно, никогда не будет вызывать эти функции n * 10M раз с теми же параметрами.

const mathCompare = (a1, a2, b1, b2) => {
    return Math.abs(Math.log(a1/a2)) < Math.abs(Math.log(b1/b2));
};

const logicCompare = (a1, a2, b1, b2) => {
    return (a1 > a2 ? a1/a2 : a2/a1) < (b1 > b2 ? b1/b2 : b2/b1);
};

const compareRawRandom = (f1, f2, maxI, maxJ) => {
    let randoms = [...Array(maxJ + 3)].map(()=>Math.floor(Math.random()*10));
    for(let i = 0; i < maxI; i++) {
        let j,
            a = performance.now();
        for(j = 0; j < maxJ; j++) {
            f1(randoms[j], randoms[j + 1], randoms[j + 2], randoms[j + 3]);
        }
        let b = performance.now();
        for(j = 0; j < maxJ; j++) {
            f2(randoms[j], randoms[j + 1], randoms[j + 2], randoms[j + 3]);
        }
        let c = performance.now();
        console.warn( j + " iterations:\n" + f1.name + ": " + (b-a) + " ms\n" + f2.name + ": " + (c-b) + " ms\n" + "delta: " + ((b-a) - (c-b)) + " ms");
    }
}

const runRawRandom = (i) => {
    compareRawRandom(mathCompare, logicCompare, 4, i);
};

const jsCompareRandom = (f1, f2, iterations) => {
    let randoms = [...Array(iterations + 3)].map(()=>Math.floor(Math.random()*10));
    let a = jsPerfRandom(f1, iterations, randoms);
    let b = jsPerfRandom(f2, iterations, randoms);
    console.warn( iterations + " iterations:\n" + f1.name + ": " + a + " ms\n" + f2.name + ": " + b + " ms\n" + "delta: " + (a-b) + " ms");
}

const jsPerfRandom = (f, iterations, randoms) => { 
    let start = performance.now();

    for(let i = 0; i < iterations; i++) {
        f(randoms[i], randoms[i + 1], randoms[i + 2], randoms[i + 3]);
    }
    return performance.now() - start;
}

const runRandomLooped = (j) => {
    for(let i = 0; i < 4; i++) {
        jsCompareRandom(mathCompare, logicCompare, j);
    }
}

runRandomLooped(10000000);
runRawRandom(10000000);

runRandomLooped () показывает тот же самый странный низкий первый 10M, установленный для mathCompare ().

  • logicCompare () : все наборы занимают ~ 280 мс
  • mathCompare () : первый набор ~ 27ms , следующие наборы ~ 800ms

runRawRandom () менее разложенная версия, выполняющая точно такие же вычисления, однако снова стабилизируется после 2 комплектов по 10м. Но на этот раз обе функции показывают одинаковую производительность ~ 23 мс для вызовов 10M.

Это отображается только в браузерах Chrome / Chromium. Проверено на:

  • Chrome 79.0.3945.130
  • Vivaldi 2.6.1566.49 (используется V8 7.5.288.30)

Я также проверял на Firefox 72.0.2, которая показала постоянную производительность по сетам и обоим способам зацикливания.

  • logicCompare () : ~ 110ms
  • mathCompare () : ~ 35ms

Я использую AMD FX-8350 на текущей Win10.

Я думаю, это как-то связано с оптимизацией V8 во время выполнения, но я не ожидал, что производительность go в этом случае.

1 Ответ

3 голосов
/ 06 февраля 2020

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

Прежде всего:

Неожиданная де-оптимизация ...

Нет, здесь не проблема деоптимизации (это очень специфический c термин с очень конкретным c значением). Вы имеете в виду «замедление».

... при большом числе итераций

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


Один механизм, который нужно осознавать of is "замена в стеке": функции с (очень) длительными циклами будут оптимизированы, пока выполняется l oop. Это не имеет смысла делать это в фоновом потоке, поэтому выполнение прерывается, в то время как оптимизация происходит в основном потоке. Если код после l oop еще не выполнен и поэтому не имеет обратной связи по типу, оптимизированный код будет отброшен («деоптимизирован»), как только выполнение достигнет конца l oop, для собирать тип обратной связи при выполнении неоптимизированного байт-кода. В случае другого продолжительного l oop, как в примере здесь, тот же самый танец OSR-затем-deopt будет повторен. Это означает, что некоторая нетривиальная доля того, что вы измеряете, это время оптимизации. Это объясняет большую часть различий, которые вы видите в runRawRandom до того, как времена стабилизируются.

Еще один эффект, о котором следует знать, это встраивание. Чем меньше и быстрее рассматриваемая функция, тем больше накладные расходы на вызовы, которых избегают, когда вы пишете эталонный тест, чтобы функция могла быть встроенной. Кроме того, встраивание часто открывает дополнительные возможности оптимизации: в этом случае компилятор может видеть после встраивания, что результат сравнения никогда не используется, поэтому он полностью исключает все сравнения. Это объясняет, почему runRandomLooped намного медленнее, чем runRawRandom: последние тестируют пустые циклы. Самая первая итерация первого варианта - «быстрая» (= пустая), потому что V8 в этой точке вставляет mathCompare для вызова f(...) в jsPerfRandom (потому что это единственная функция, которую он там когда-либо видел), но вскоре после того, как она реализуется " К сожалению, здесь вызываются различные функции ", поэтому он отключается и не пытается снова подключиться при последующих попытках оптимизации.

Если вам важны детали, вы можете использовать некоторую комбинацию флагов --trace-opt --trace-deopt --trace-osr --trace-turbo-inlining --print-opt-code --code-comments исследовать поведение в глубине. Имейте в виду, что хотя это упражнение, вероятно, будет стоить вам значительного времени, то, что вы можете узнать из поведения микробенчмарка, по всей вероятности, не будет иметь отношения к реальным случаям использования.

Для иллюстрации:

  • у вас есть один микробенчмарк, который доказывает несомненное , что mathCompare намного медленнее logicCompare
  • у вас есть еще один микробенчмарк, доказывает, без сомнения , что оба имеют одинаковую производительность
  • ваши совокупные наблюдения доказывают, без сомнения , что производительность снижается, когда V8 решает оптимизировать вещи

На практике все три наблюдения ложны (что не слишком удивительно, учитывая, что два из них являются прямыми противоречиями друг друга):

  • "быстрые результаты" не отражает реальное поведение, потому что они могут мертвым кодом устранить рабочую нагрузку, которую вы пытались измерить
  • "медленные результаты" не отражает реальное поведение, потому что конкретный c способ написания эталонного теста препятствовал встраиванию крошечной функции (которая практически всегда вставлялась бы в реальный код)
  • предполагаемое «снижение производительности» было просто артефакт микробенчмаркинга, который вообще не был вызван неудачной / бесполезной оптимизацией и не возникнет в реальной ситуации.
...