Есть ли разница в производительности непараллельного асинхронного цикла и цикла синхронизации в Node.js?

Я преобразовал код JavaScript для Node.js (v10.13.0), который ранее был синхронным, в асинхронный код с использованием async/await. То, что я заметил впоследствии, было ухудшением производительности примерно в 3 раза медленнее времени выполнения программы.

Есть ли снижение производительности при преобразовании цепочки вызовов синхронных функций в вызовы асинхронных функций?

Упрощенный пример

Изменение синхронного кода

function fn1() {  
   return 1;
}

function fn2() { 
   return fn1();
}

(function() {
  const result = fn2();
});

в асинхронный код:

async function fn1() {  
   return 1;
}

async function fn2() { 
   return await fn1();
}

(async function() {
   const result = await fn2();
})();

Есть ли магия циклов событий, которая может замедлить последний код в веб-приложении Node.js?

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

Mark 10.04.2019 03:36
async и await — синтаксический сахар. Собственная реализация будет генерировать кучу дополнительного кода (функция генератора), который будет отслеживать, куда двигаться дальше, когда асинхронная операция завершится. Это увеличивает нагрузку и снижает производительность.
JohanP 10.04.2019 03:45

@MarkMeyer, чтобы прояснить вариант использования: в моем сценарии асинхронная операция выборки из базы данных должна быть реализована в fn1(), а вызывающие функции fn1() должны дождаться возврата результата. Поэтому я должен как-то изменить функции в цепочке вызовов, например fn2(). Я не пытаюсь сделать что-то асинхронным без причины. :) Было бы эффективнее передать функцию обратного вызова?

Martin Löper 10.04.2019 16:44

@MartinLöper Если вы ничего не делаете, удалите объявление await. т.е. если async является вашим асинхронным вызовом БД, вы можете пропустить вызов и напрямую вернуть fn(1), если вам НЕ нужно что-то делать с результатом после возврата вызова БД. Это означает, что вы можете изменить Promise, чтобы он не был fn2(), и напрямую вернуть async. Единственное место, где вам нужно ключевое слово fn1(), это когда вы хотите получить результат от асинхронного вызова, и это находится в вашем IIFE.

JohanP 11.04.2019 01:05

Казалось бы совершенно очевидным, что добавление ненужных накладных расходов к функциям замедляет их работу. Это действительно неожиданно? А почему вы вообще "код рефакторинга, который ранее был синхронным, в асинхронный код"?

Bergi 11.12.2021 04:52

@ Берги Хороший вопрос! Я хотел, чтобы чистая функция получения (условно) выполняла вызов базы данных. Допустим, 1 из 1000 звонков — это обращение к базе данных. Остаток возвращает значение из памяти. Функция вызывалась дюжину раз как часть алгоритма перебора строк, который по замыслу представляет собой большой цикл for. Функция, ставшая асинхронной, вызывалась очень глубоко в стеке вызовов, потому что это была какая-то служебная функция. Мне пришлось сделать все функции в цепочке вызовов асинхронными, чтобы они могли вызывать эту служебную функцию. Это привело к резкому замедлению работы, что и послужило мотивом для этого поста SO.

Martin Löper 21.12.2021 15:37

@MartinLöper Геттер, особенно тот, который вы называете «чистым», определенно не должен вызывать базу данных. Загрузите данные заранее, а затем запустите на них свой алгоритм.

Bergi 21.12.2021 18:19

Вы определенно правы, что вам не следует писать такой код. Мне просто интересно, что произойдет, если вы все же сделаете это. Интересно, что этот пост привел к обсуждению того, что должны делать JS и JIT-компилятор и что является частью ответственности разработчика. Очень ценю ваше мнение по этой теме, ребята :)

Martin Löper 22.12.2021 19:38
Поведение ключевого слова "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) для оценки ваших знаний,...
2
8
276
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Вот более продвинутый тест, который вычисляет ряды Фибоначчи с синхронной или асинхронной функцией:

async function benchmark(M = 1000000, N = 100) {
    function fibonacci_sync(num) {
        let a = 1, b = 0, temp
        while (num >= 0) {
            temp = a; a = a + b; b = temp; num--
        }
        return b
    }
    async function fibonacci_async(num) {
        let a = 1, b = 0, temp
        while (num >= 0) {
            temp = a; a = a + b; b = temp; num--
        }
        return b
    }
    timeitSync  ('sync',  M,       () => {for(let i = 0; i < N; i++) fibonacci_sync(i)})
    await timeit('async', M, async () => {for(let i = 0; i < N; i++) await fibonacci_async(i)})
}

Пример времени выполнения в node.js — async оказывается в 2,8 раза медленнее:

sync:    4.753s
async:  13.359s

С большим M, но меньшим N = 10 вместо N=100 (более короткий расчет, поэтому ожидания оказывают большее влияние), асинхронная функция становится в 14,5 раз медленнее (упс!!):

sync:   0.499s
async:  7.258s

Это на узле v16.13.1. Эталонный тест был вдохновлен этим сообщением: https://madelinemiller.dev/blog/javascript-promise-overhead/

Для полноты картины вот функции timeit, использованные выше:

async function timeit(label, repeat, fun) {
    console.time(label)
    for (let i = 0; i < repeat; i++) await fun()
    console.timeEnd(label)
}
function timeitSync(label, repeat, fun) {
    console.time(label)
    for (let i = 0; i < repeat; i++) fun()
    console.timeEnd(label)
}

При измерении fibonacci_sync с помощью async timeit вместо timeitSync время выполнения растет с 0.499s до 1.2s в последнем примере, что является еще одним подтверждением того, что async вызывает сильное замедление.

Так что да, действительно, асинхронные вызовы могут привести к ОГРОМНОМУ снижению производительности, даже на порядок. Каждый вызов должен проходить через очередь событий, управление которой создает значительные накладные расходы. Это определенно следует учитывать при реализации кода, который плотно набит асинхронными функциями.

Учитывая, насколько «заразна» парадигма async — с одной низкоуровневой функцией, являющейся асинхронной, все вызывающие ее функции вверх по дереву также должны быть асинхронными — я был бы рад, что JS вводит оптимизации, позволяющие await... выполняться мгновенно в некоторых (большинстве ) случаев, а не помещаться в очередь снова и снова через каждую пару инструкций. Это может принести пользу всем сценариям, где await завернуто в условное выражение и редко требует фактической остановки функции, но все же требует, чтобы функция была объявлена ​​как «асинхронная», независимо от того, как часто достигается «ожидание».

«Каждый вызов должен проходить через очередь событий» — это не вызовы функций, это возобновления кода awaiting после разрешения промисов, которые проходят через очередь микрозадач. "JS вводит оптимизации, позволяющие мгновенно выполнять await... в некоторых (большинстве) случаях." - нет, нет и не будет. То, что await-выражения гарантируют асинхронность (точно так же, как .then()-вызовы, для которых они являются синтаксическим сахаром), является важной особенностью асинхронных функций.

Bergi 11.12.2021 04:59

Ключевой вывод из статья, которую вы связали: "Самое простое решение здесь — выполнять выборку данных или другие асинхронные операции ближе к корню приложения и передавать полученные данные вниз. Часто структуры программ, которые включают глубоко вложенные асинхронные/ожидающие пути, демонстрируют плохое разделение задач. В идеале одна система не должна загружать и использовать данные; вместо этого он должен получать данные из другой системы, которая их загружает. Эта структура также имеет дополнительное преимущество, заключающееся в том, что ее гораздо легче тестировать."

Bergi 11.12.2021 05:04

@Bergi: в некоторых случаях тот факт, что выражения ожидания гарантируют асинхронность... является ошибкой, а не функцией - приведенный выше тест показывает, почему. Чего программист обычно хочет достичь с помощью «ожидания», так это разрешать, а не обеспечивать соблюдение, асинхронности в этом конкретном месте. Задача интерпретатора и JIT-компилятора заключается в том, чтобы решить, желательна ли отсрочка выполнения в данный момент или нет - именно для такой оптимизации и нужен JIT-компилятор. Нет, я не думаю, что возложение ответственности за эти оптимизации на программистов поможет им создавать более качественное программное обеспечение.

Marcin Wojnarski 11.12.2021 17:29

Интересная дискуссия о том, что должен или не должен делать движок JS, ребята! Я совершенно не собирался навязывать асинхронное поведение при расширении нашей кодовой базы. Я просто хотел разрешить асинхронность глубоко вложенному методу, который вызывался десятки раз из-за некоторого цикла for.

Martin Löper 21.12.2021 15:32

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