paint-brush
NodeJS: в 4,8 раза быстрее, если вы вернетесь к обратным вызовам!к@gemmablack
20,099 чтения
20,099 чтения

NodeJS: в 4,8 раза быстрее, если вы вернетесь к обратным вызовам!

к Gemma Black9m2024/02/12
Read on Terminal Reader

Слишком долго; Читать

Обратные вызовы выполняются в 4,8 раза быстрее при их параллельном выполнении по сравнению с async/await. И только в 1,9 раза быстрее, когда мы запускаем последовательные обратные вызовы.
featured image - NodeJS: в 4,8 раза быстрее, если вы вернетесь к обратным вызовам!
Gemma Black HackerNoon profile picture

Да, я сказал это!


Обратные вызовы выполняются в 4,8 раза быстрее, если они выполняются параллельно с async/await. И только в 1,9 раза быстрее, когда мы запускаем последовательные обратные вызовы.


Я несколько изменил эту статью после того, как получил несколько полезных и добрых комментариев по поводу моего хитроумного теста. 😂🙏


Спасибо тебе Рикардо Лопес и Райан По за то, что нашли время направить показатели в правильном направлении. Моей первой оплошностью было то, что я на самом деле не дождался завершения выполнения кода, что безумно исказило результаты. Во-вторых, я сравнивал параллельное и последовательное время выполнения, что делает тесты бесполезными.


Итак, это второй раунд, который устраняет мои первоначальные ошибки. Раньше я говорил:


Изначально я писал, что NodeJS будет в 34,7 раза быстрее, если вернуться к обратным вызовам! 🤣 Неправильно.


Не так впечатляюще, как мои предыдущие плохие тесты (см. комментарии для контекста), но все же значительная разница.

Итак, что именно я тестировал?

Я сравнивал обратные вызовы с обещаниями и async/await при чтении файла 10 000 раз. И, возможно, это глупый тест, но я хотел знать, что быстрее при вводе-выводе.


Затем я наконец сравнил обратные вызовы в Node.js с Go!


Теперь угадайте, кто победил?


Я не буду злым. TLDR . Голанг!


Ниже - лучше. Результаты в ms .


Итак, настоящие бенчмаркеры? Полегче со мной в этом вопросе. Но, пожалуйста, оставляйте свои комментарии, чтобы я стал лучше.

Все говорят, что Node.js медленный!

И это меня бесит.


Ведь что значит медленно? Как и все тесты, мой является контекстным.


Я начал читать о Цикл событий , просто чтобы хотя бы начать понимать как это работает .


Но главное, что я понял, это то, что Node.js передает задачи ввода-вывода в очередь, которая находится вне основного исполняемого потока Node.js. Эта очередь работает чистый С . Несколько потоков потенциально могут обрабатывать эти операции ввода-вывода. И именно здесь Node.js может проявить себя, обрабатывая ввод-вывод.


Однако промисы обрабатываются в основном единственном исполняемом потоке. А async/await — это хорошо, обещает, но теперь с добавленной блокировкой.

Цикл событий, состоящий из 6 различных очередей. Откуда: https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Так обратные вызовы быстрее, чем обещания?

Давайте проверим это.


Прежде всего. Моя машина ! Дополнения к работе с Камма . Важно отметить, с какими ресурсами мы работаем. Много памяти и процессора.

 MacBook Pro (14-inch, 2021) Chip Apple M1 Pro Memory 32 GB Cores 10 NodeJS v20.8.1

Итак, у нас есть файл text.txt с исходным сообщением Hello, world .

 echo "Hello, world" > text.txt

И мы будем читать этот текстовый файл, используя собственный Node.js, что означает отсутствие зависимостей от узловых модулей, поскольку мы не хотим снижать скорость при работе с самыми тяжелыми объектами во вселенной.

Обратные вызовы

Параллельные обратные вызовы

Во-первых, давайте начнем с параллельных обратных вызовов. Меня интересует, насколько быстро можно прочитать один и тот же файл, причем максимально быстро, весь сразу. А что быстрее параллельного?

 // > file-callback-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs"; test('reading file 10,000 times with callback parallel', (t, done) => { let count = 0; for (let i = 0; i < 10000; i++) { fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => { assert.strictEqual(data, "Hello, world"); count++ if (count === 10000) { done() } }) } });

Последовательные обратные вызовы

Во-вторых, у нас снова есть обратные вызовы, но последовательные (вернее блокирующие). Меня интересует, насколько быстро можно прочитать один и тот же файл последовательно. Я давно не делал обратных вызовов, поэтому было интересно попробовать еще раз. Хоть и выглядит это некрасиво.

 // > file-callback-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs"; let read = (i, callback) => { fs.readFile("./text.txt", { encoding: 'utf-8'}, (err, data) => { assert.strictEqual(data, "Hello, world"); i += 1 if (i === 10000) { return callback() } read(i, callback) }) } test('reading file 10,000 times with callback blocking', (t, done) => { read(0, done) });

Асинхронный/ожидающий

Затем у нас есть async/await. Мой любимый способ работы с Nodejs.

Параллельный асинхронный/ожидающий

Это настолько параллельно, насколько это возможно с помощью async/await. Я загружаю все операции readFile в массив и жду их всех, используя Promise.all .

 // > file-async-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with async parallel', async (t) => { let allFiles = [] for (let i = 0; i < 10000; i++) { allFiles.push(fs.readFile("./text.txt", { encoding: 'utf-8'})) } return await Promise.all(allFiles) .then(allFiles => { return allFiles.forEach((data) => { assert.strictEqual(data, "Hello, world"); }) }) });

Последовательный асинхронный/ожидающий

Это было самое простое и лаконичное письмо.

 // > file-async-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with async blocking', async (t) => { for (let i = 0; i < 10000; i++) { let data = await fs.readFile("./text.txt", { encoding: 'utf-8'}) assert.strictEqual(data, "Hello, world"); } });

Обещания

Наконец, у нас есть промисы без async/await. Я давно перестал использовать их в пользу async/await , но меня интересовало, насколько они производительны или нет.

Параллельные обещания

 // > file-promise-parallel.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with promise parallel', (t, done) => { let allFiles = [] for (let i = 0; i < 10000; i++) { allFiles.push(fs.readFile("./text.txt", { encoding: 'utf-8'})) } Promise.all(allFiles) .then(allFiles => { for (let i = 0; i < 10000; i++) { assert.strictEqual(allFiles[i], "Hello, world"); } done() }) });

Последовательные обещания.

Опять же, мы хотим дождаться выполнения всех операций readFile .

 // > file-promise-blocking.test.mjs import test from 'node:test'; import assert from 'node:assert'; import fs from "node:fs/promises"; test('reading file 10,000 times with promises blocking', (t, done) => { let count = 0; for (let i = 0; i < 10000; i++) { let data = fs.readFile("./text.txt", { encoding: 'utf-8'}) .then(data => { assert.strictEqual(data, "Hello, world") count++ if (count === 10000) { done() } }) } });

И вуаля! Результаты 🎉! Я даже пробежал его несколько раз, чтобы лучше читать.

Я запускал каждый тест, выполняя:

 node --test <file>.mjs

Чтение файла 10 000 раз с помощью обратных вызовов происходит более чем в 5,8 раз быстрее, чем при параллельном использовании async/await! Это также в 4,7 раза быстрее, чем при параллельном выполнении обещаний!


Итак, в мире Node.js обратные вызовы более производительны!

Теперь Go быстрее, чем Node.js?

Ну, я не пишу на Go, так что это может быть действительно ужасный код, потому что я попросил ChatGPT помочь мне, и тем не менее, он кажется довольно приличным.


Хей-хо. Пойдем. Наш код Голанга.

 package main import ( "fmt" "io/ioutil" "time" ) func main() { startTime := time.Now() for i := 0; i < 10000; i++ { data, err := ioutil.ReadFile("./text.txt") if err != nil { fmt.Printf("Error reading file: %v\n", err) return } if string(data) != "Hello, world" { fmt.Println("File content mismatch: got", string(data), ", want Hello, world") return } } duration := time.Since(startTime) fmt.Printf("Test execution time: %v\n", duration) }

И мы запускаем это так:

 go run main.go

И результаты?

 Test execution time: 58.877125ms

🤯 Go в 4,9 раза быстрее, чем Node.js, при использовании последовательных обратных вызовов. Node.js приближается только к параллельному выполнению.


Node.js Async/await в 9,2 раза медленнее, чем Go.


Так да. Node.js медленнее. Тем не менее, 10 000 файлов менее чем за 300 мс не заслуживают насмешек. Но я был ошеломлен скоростью Go!

Теперь просто примечание. Есть ли у меня плохие ориентиры?

У меня действительно были ужасные тесты. Еще раз спасибо Рикардо и Райану.


Да, я сделал. Надеюсь, теперь им стало лучше.


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


Я также не знаю, сколько потоков использует Node.js.


Я не знаю, как ядра моего процессора влияют на производительность Go и Node.js.


Я мог бы просто арендовать машину AWS с одним ядром и сравнить.


Это потому, что я на Mac M1?


Как Node.js будет работать в Linux или... Windows? 😱


И есть практичность: да, чтение файла — это одно, но в какой-то момент вам все равно придется подождать, пока файл будет прочитан, чтобы что-то сделать с данными в файле. Таким образом, скорость основного потока по-прежнему очень важна.

Итак, вы действительно хотите использовать обратные вызовы?

Я имею в виду, ты действительно, действительно этого хочешь?


Я не знаю. Я определенно не хочу никому говорить, что делать.

Но мне нравится чистый синтаксис async/awaits.


Они выглядят лучше.


Они читают лучше.


Я знаю, что здесь все субъективно, но я помню ад обратных вызовов и был благодарен, когда появились обещания. Это сделало Javascript терпимым.


Теперь Golang явно быстрее, чем Node.js в оптимальном состоянии, с обратными вызовами и с async/await, в 9,2 раза! Так что, если нам нужна хорошая читаемость и производительность, Golang — победитель. Хотя мне бы хотелось узнать, как Golang выглядит под капотом.


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

Итак, чтобы выйти

Node.js медленный? Или мы просто используем Node.js в медленном режиме?


Вероятно, там, где важна производительность, Golang стоит того. Я обязательно буду больше использовать Golang в будущем.


Также появляется здесь .