Да, я сказал это!
Обратные вызовы выполняются в 4,8 раза быстрее, если они выполняются параллельно с async/await. И только в 1,9 раза быстрее, когда мы запускаем последовательные обратные вызовы.
Я несколько изменил эту статью после того, как получил несколько полезных и добрых комментариев по поводу моего хитроумного теста. 😂🙏
Спасибо тебе
Рикардо Лопес иРайан По за то, что нашли время направить показатели в правильном направлении. Моей первой оплошностью было то, что я на самом деле не дождался завершения выполнения кода, что безумно исказило результаты. Во-вторых, я сравнивал параллельное и последовательное время выполнения, что делает тесты бесполезными.
Итак, это второй раунд, который устраняет мои первоначальные ошибки. Раньше я говорил:
Изначально я писал, что NodeJS будет в 34,7 раза быстрее, если вернуться к обратным вызовам! 🤣 Неправильно.
Не так впечатляюще, как мои предыдущие плохие тесты (см. комментарии для контекста), но все же значительная разница.
Я сравнивал обратные вызовы с обещаниями и async/await при чтении файла 10 000 раз. И, возможно, это глупый тест, но я хотел знать, что быстрее при вводе-выводе.
Затем я наконец сравнил обратные вызовы в Node.js с Go!
Теперь угадайте, кто победил?
Я не буду злым. TLDR . Голанг!
Ниже - лучше. Результаты в
ms
.
Итак, настоящие бенчмаркеры? Полегче со мной в этом вопросе. Но, пожалуйста, оставляйте свои комментарии, чтобы я стал лучше.
И это меня бесит.
Ведь что значит медленно? Как и все тесты, мой является контекстным.
Я начал читать о
Но главное, что я понял, это то, что Node.js передает задачи ввода-вывода в очередь, которая находится вне основного исполняемого потока Node.js. Эта очередь работает
Однако промисы обрабатываются в основном единственном исполняемом потоке. А async/await — это хорошо, обещает, но теперь с добавленной блокировкой.
Давайте проверим это.
Прежде всего. Моя машина ! Дополнения к работе с
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, так что это может быть действительно ужасный код, потому что я попросил 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 в будущем.
Также появляется здесь .