Почему JavaScript с первого раза так быстро выполняет обратные вызовы в цикле for?

Следующий код с аргументом 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

Почему?

Вы тестировали это на разных движках JavaScript (например, Chrome/Node (V8) или Firefox или Bun (JSCore))?

AKX 25.07.2024 11:23

Интересно, что в SpiderMonkey и в V8 наблюдается одинаковое поведение. Следует отметить одну вещь: если вы не используете одну и ту же функцию каждый раз повторно, а вместо этого дублируете их (fn1 = (...) => ...;fn2 = (...) = ;fn3 = (...) => ) или если вы используете одну и ту же функцию обратного вызова (так что cb = () => {}; fn(length, label, cb), то все будет действовать быстро. Это своего рода звоночек и Я думаю, что этот вопрос уже задавался здесь раньше... но я не уверен, что смогу найти вопрос/ответ, если он существует.

Kaiido 25.07.2024 13:43

Вероятно, это связано с mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html. При первом запуске он может просто встроить callback и оптимизировать проверку. При последующих запусках он замечает, что существуют разные значения callback, поэтому ему приходится сделать реальный вызов.

Bergi 25.07.2024 15:59

Что именно означает «очень мало интервалов»? Пожалуйста, сообщите фактическое время выполнения, которое вы получите.

Bergi 25.07.2024 16:01

Если вы определите стрелочную функцию как const clb = () => {};, а затем передадите ее fn(length, "1", clb), разница исчезнет.

0stone0 25.07.2024 16:10

@Bergi Эта статья mrale.ph связана, но не совпадает: в ней говорится о мономорфизме формы объекта, тогда как эффект здесь обусловлен целевым мономорфизмом вызова (или его отсутствием), как предполагает ваш собственный комментарий. -- Я предполагаю, что «очень мало интервалов» означает «очень мало времени».

jmrk 25.07.2024 16:27

@jmrk Да, я имел в виду связанное, это всего лишь первая статья, которую я нашел о мономорфизме и встроенных кешах (и которая по-прежнему превосходна, несмотря на свой возраст). Я уверен, что вы сможете объяснить конкретные детали гораздо лучше, с нетерпением жду вашего ответа :-)

Bergi 25.07.2024 16:33
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
11
7
725
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Короче говоря: это из-за встраивания.

Когда при вызове, таком как callback(), вызывается только одна целевая функция, а содержащая функция («fn» в данном случае) оптимизируется, тогда оптимизирующий компилятор (обычно) решает встроить эту цель вызова. Таким образом, в быстрой версии фактический вызов не выполняется, вместо этого встраивается пустая функция.
Когда вы затем вызываете другие обратные вызовы, старый оптимизированный код необходимо выбросить («деоптимизировать»), поскольку теперь он некорректен (если новый обратный вызов имеет другое поведение), а при повторной оптимизации через некоторое время эвристика встраивания решает, что встраивание нескольких возможных целей, вероятно, не стоит затрат (поскольку встраивание, хотя иногда и дает большие преимущества в производительности, также имеет определенные затраты), поэтому он ничего не встраивает. Вместо этого сгенерированный оптимизированный код теперь будет выполнять реальные вызовы, и вы увидите цену этого.

Как заметил @0stone0, когда вы передаете тот же обратный вызов при втором вызове fn, деоптимизация не требуется, поэтому исходно сгенерированный оптимизированный код (в который встроен этот обратный вызов) можно продолжать использовать. Определение трех разных обратных вызовов с одним и тем же (пустым) исходным кодом не считается «одним и тем же обратным вызовом».

Кстати, этот эффект наиболее выражен в микробенчмарках; хотя иногда это также видно в более реальном коде. Это, безусловно, обычная ловушка, в которую попадают микробенчмарки и дают запутанные/вводящие в заблуждение результаты.

Во втором эксперименте, когда нет callback, то, конечно, часть выражения callback && уже выйдет из строя, и ни один из трех вызовов fn не будет вызывать (или встраивать) какие-либо обратные вызовы, потому что обратных вызовов нет.

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