Следующий код с аргументом callback
выполняется быстрее в первом цикле.
const fn = (length, label, callback) => {
console.time(label);
for (let i = 0; i < length; i++) {
callback && callback(i);
}
console.timeEnd(label);
};
const length = 100000000;
fn(length, "1", () => {}) // very few intervals
fn(length, "2", () => {}) // regular
fn(length, "3", () => {}) // regular
а затем я удалил третий аргумент callback
, и время их выполнения очень близко:
const fn = (length, label, callback) => {
console.time(label);
for (let i = 0; i < length; i++) {
callback && callback(i);
}
console.timeEnd(label);
};
const length = 100000000;
fn(length, "1") // regular
fn(length, "2") // regular
fn(length, "3") // regular
Почему?
Интересно, что в SpiderMonkey и в V8 наблюдается одинаковое поведение. Следует отметить одну вещь: если вы не используете одну и ту же функцию каждый раз повторно, а вместо этого дублируете их (fn1 = (...) => ...;
fn2 = (...) = ;
fn3 = (...) =>
) или если вы используете одну и ту же функцию обратного вызова (так что cb = () => {}; fn(length, label, cb)
, то все будет действовать быстро. Это своего рода звоночек и Я думаю, что этот вопрос уже задавался здесь раньше... но я не уверен, что смогу найти вопрос/ответ, если он существует.
Вероятно, это связано с mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html. При первом запуске он может просто встроить callback
и оптимизировать проверку. При последующих запусках он замечает, что существуют разные значения callback
, поэтому ему приходится сделать реальный вызов.
Что именно означает «очень мало интервалов»? Пожалуйста, сообщите фактическое время выполнения, которое вы получите.
Если вы определите стрелочную функцию как const clb = () => {};
, а затем передадите ее fn(length, "1", clb)
, разница исчезнет.
@Bergi Эта статья mrale.ph связана, но не совпадает: в ней говорится о мономорфизме формы объекта, тогда как эффект здесь обусловлен целевым мономорфизмом вызова (или его отсутствием), как предполагает ваш собственный комментарий. -- Я предполагаю, что «очень мало интервалов» означает «очень мало времени».
@jmrk Да, я имел в виду связанное, это всего лишь первая статья, которую я нашел о мономорфизме и встроенных кешах (и которая по-прежнему превосходна, несмотря на свой возраст). Я уверен, что вы сможете объяснить конкретные детали гораздо лучше, с нетерпением жду вашего ответа :-)
Короче говоря: это из-за встраивания.
Когда при вызове, таком как callback()
, вызывается только одна целевая функция, а содержащая функция («fn
» в данном случае) оптимизируется, тогда оптимизирующий компилятор (обычно) решает встроить эту цель вызова. Таким образом, в быстрой версии фактический вызов не выполняется, вместо этого встраивается пустая функция.
Когда вы затем вызываете другие обратные вызовы, старый оптимизированный код необходимо выбросить («деоптимизировать»), поскольку теперь он некорректен (если новый обратный вызов имеет другое поведение), а при повторной оптимизации через некоторое время эвристика встраивания решает, что встраивание нескольких возможных целей, вероятно, не стоит затрат (поскольку встраивание, хотя иногда и дает большие преимущества в производительности, также имеет определенные затраты), поэтому он ничего не встраивает. Вместо этого сгенерированный оптимизированный код теперь будет выполнять реальные вызовы, и вы увидите цену этого.
Как заметил @0stone0, когда вы передаете тот же обратный вызов при втором вызове fn
, деоптимизация не требуется, поэтому исходно сгенерированный оптимизированный код (в который встроен этот обратный вызов) можно продолжать использовать. Определение трех разных обратных вызовов с одним и тем же (пустым) исходным кодом не считается «одним и тем же обратным вызовом».
Кстати, этот эффект наиболее выражен в микробенчмарках; хотя иногда это также видно в более реальном коде. Это, безусловно, обычная ловушка, в которую попадают микробенчмарки и дают запутанные/вводящие в заблуждение результаты.
Во втором эксперименте, когда нет callback
, то, конечно, часть выражения callback &&
уже выйдет из строя, и ни один из трех вызовов fn
не будет вызывать (или встраивать) какие-либо обратные вызовы, потому что обратных вызовов нет.
Вы тестировали это на разных движках JavaScript (например, Chrome/Node (V8) или Firefox или Bun (JSCore))?