paint-brush
NodeJS: Nhanh hơn 4,8 lần nếu bạn quay lại Cuộc gọi lại!by@gemmablack
19,696
19,696

NodeJS: Nhanh hơn 4,8 lần nếu bạn quay lại Cuộc gọi lại!

Gemma Black9m2024/02/12
Read on Terminal Reader

Lệnh gọi lại nhanh hơn 4,8 lần khi chạy song song trên async/await song song. Và chỉ nhanh hơn 1,9 lần khi chúng tôi chạy lệnh gọi lại tuần tự.
featured image - NodeJS: Nhanh hơn 4,8 lần nếu bạn quay lại Cuộc gọi lại!
Gemma Black HackerNoon profile picture

Vâng, tôi đã nói rồi!


Lệnh gọi lại nhanh hơn 4,8 lần khi chạy song song trên async/await song song. Và chỉ nhanh hơn 1,9 lần khi chúng tôi chạy lệnh gọi lại tuần tự.


Tôi đã sửa đổi bài viết này một chút sau khi nhận được một số nhận xét hữu ích và tử tế về bài kiểm tra tồi của mình. 😂🙏


Cảm ơn bạn Ricardo Lopes Ryan Poe vì đã dành thời gian để điều khiển các điểm chuẩn đi đúng hướng. Sai lầm đầu tiên của tôi là tôi đã không thực sự đợi quá trình thực thi mã kết thúc, điều này đã làm sai lệch kết quả một cách điên cuồng. Thứ hai là tôi đã so sánh song song với thời gian chạy tuần tự, điều này làm cho điểm chuẩn trở nên vô giá trị.


Vì vậy, đây là vòng 2 nhằm giải quyết các lỗi ban đầu của tôi. Trước đây tôi đã nói:


Ban đầu, tôi đã viết rằng NodeJS nhanh hơn 34,7 lần nếu chúng ta quay lại gọi lại! 🤣 Sai rồi.


Không ấn tượng như các điểm chuẩn kém của tôi trước đây (và xem nhận xét để biết ngữ cảnh), nhưng vẫn là một sự khác biệt đáng kể.

Vậy chính xác thì tôi đã kiểm tra cái gì?

Tôi đã so sánh lệnh gọi lại với lời hứa và không đồng bộ/chờ đợi khi đọc tệp, 10.000 lần. Và có thể đó là một bài kiểm tra ngớ ngẩn nhưng tôi muốn biết, cái nào nhanh hơn ở I/O.


Sau đó, cuối cùng tôi đã so sánh các lệnh gọi lại trong Node.js với Go!


Bây giờ, đoán xem ai đã thắng?


Tôi sẽ không ác ý đâu. TLDR . Golang!


Thấp hơn là tốt hơn. Kết quả được tính bằng ms .


Bây giờ, điểm chuẩn thực sự? Hãy thoải mái với tôi về điều này. Nhưng xin vui lòng để lại ý kiến của bạn để làm cho tôi trở thành một người tốt hơn.

Mọi người cứ nói rằng Node.js chậm!

Và nó làm tôi khó chịu.


Bởi vì chậm có nghĩa là gì? Như với tất cả các điểm chuẩn, điểm chuẩn của tôi là theo ngữ cảnh.


Tôi bắt đầu đọc về Vòng lặp sự kiện , chỉ để bắt đầu hiểu làm thế nào nó hoạt động .


Nhưng điều chính mà tôi hiểu là Node.js chuyển các tác vụ I/O vào một hàng đợi nằm bên ngoài luồng thực thi Node.js chính. Hàng đợi này chạy tiếp C nguyên chất . Một số luồng có khả năng xử lý các hoạt động I/O này. Và đó là lúc Node.js có thể tỏa sáng, xử lý I/O.


Tuy nhiên, các lời hứa sẽ được xử lý trong luồng thực thi chính, duy nhất. Và async/await, cũng tốt, hứa hẹn nhưng bây giờ đã thêm tính năng chặn.

Vòng lặp sự kiện bao gồm 6 hàng đợi khác nhau. Từ: https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

Vậy lệnh gọi lại có nhanh hơn lời hứa không?

Hãy đưa nó vào thử nghiệm.


Đầu tiên. Máy của tôi! Bổ sung khi làm việc với Nghiệp . Điều quan trọng cần lưu ý là chúng tôi đang làm việc với những tài nguyên nào. Nhiều bộ nhớ và CPU.

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

Như vậy chúng ta đã có file text.txt với thông điệp gốcHello, world .

 echo "Hello, world" > text.txt

Và chúng ta sẽ đọc tệp văn bản này bằng cách sử dụng Node.js gốc, nghĩa là không phụ thuộc vào mô-đun nút vì chúng ta không muốn giảm tốc độ với các vật thể nặng nhất trong vũ trụ.

Cuộc gọi lại

Cuộc gọi lại song song

Đầu tiên, hãy bắt đầu với các lệnh gọi lại song song . Tôi quan tâm đến việc làm thế nào để có thể đọc cùng một tệp nhanh nhất có thể, cùng một lúc. Và cái gì nhanh hơn song song?

 // > 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() } }) } });

Gọi lại tuần tự

Thứ hai, chúng ta lại có các cuộc gọi lại, nhưng tuần tự (hay đúng hơn là chặn). Tôi quan tâm đến việc có thể đọc tuần tự cùng một tệp nhanh như thế nào. Đã lâu rồi chưa thực hiện cuộc gọi lại gọi lại, việc thử lại thật thú vị. Mặc dù trông nó không đẹp lắm.

 // > 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) });

Không đồng bộ/Đang chờ

Sau đó chúng ta có async/await. Cách làm việc yêu thích của tôi với Nodejs.

Không đồng bộ/chờ song song

Nó song song nhất có thể với async/await. Tôi tải tất cả các hoạt động readFile vào một mảng và chờ đợi tất cả chúng bằng cách sử dụng 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"); }) }) });

Không đồng bộ/Đang chờ tuần tự

Đây là cách viết dễ dàng và ngắn gọn nhất.

 // > 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"); } });

Lời hứa

Cuối cùng, chúng ta có những lời hứa không có async/await. Tôi đã ngừng sử dụng chúng từ lâu để ủng hộ async/await nhưng tôi quan tâm đến việc liệu chúng có hoạt động hiệu quả hay không.

Lời hứa song song

 // > 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() }) });

Những lời hứa tuần tự

Một lần nữa, chúng tôi muốn chờ thực thi tất cả các hoạt động 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() } }) } });

Và Voila! Kết quả 🎉! Tôi thậm chí còn chạy nó vài lần để đọc tốt hơn.

Tôi đã chạy từng bài kiểm tra bằng cách thực hiện:

 node --test <file>.mjs

Đọc tệp 10.000 lần với lệnh gọi lại nhanh hơn 5,8 lần so với async/await song song! Nó cũng nhanh hơn 4,7 lần so với những lời hứa song song!


Vì vậy, ở vùng đất Node.js, các cuộc gọi lại hiệu suất cao hơn!

Bây giờ Go có nhanh hơn Node.js không?

Chà, tôi không viết bằng Go, vì vậy đây có thể là mã thực sự khủng khiếp vì tôi đã nhờ ChatGPT giúp đỡ nhưng nó có vẻ khá ổn.


Ê này. Đi nào. Mã Golang của chúng tôi.

 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) }

Và chúng tôi chạy nó như vậy:

 go run main.go

Và kết quả?

 Test execution time: 58.877125ms

🤯 Go nhanh hơn 4,9 lần so với Node.js khi sử dụng lệnh gọi lại tuần tự. Node.js chỉ đạt được hiệu quả khi thực thi song song.


Node.js Async/await chậm hơn 9,2 lần so với Go.


Vì vậy, có. Node.js chậm hơn. Tuy nhiên, 10.000 tệp có thời lượng dưới 300 mili giây không phải là điều đáng chê trách. Nhưng tôi đã phải khiêm tốn trước tốc độ của Go!

Bây giờ chỉ là một lưu ý phụ. Tôi có điểm chuẩn xấu không?

Tôi thực sự đã có Điểm chuẩn khủng khiếp. Một lần nữa xin cảm ơn Ricardo và Ryan.


Vâng, tôi đã làm vậy. Hy vọng bây giờ họ đã tốt hơn.


Nhưng bạn có thể hỏi, ai thực sự sẽ đọc đi đọc lại cùng một tập tin? Nhưng để kiểm tra tương đối giữa mọi thứ, tôi hy vọng đó là một so sánh hữu ích.


Tôi cũng không biết Node.js đang sử dụng bao nhiêu luồng.


Tôi không biết lõi CPU của mình ảnh hưởng như thế nào đến hiệu suất của Go so với Node.js.


Tôi chỉ có thể thuê một máy AWS có một lõi và so sánh.


Có phải vì tôi đang dùng Mac M1 không?


Node.js sẽ hoạt động như thế nào trên Linux hoặc...Windows? 😱


Và tính thực tế là, vâng, đọc tệp là một chuyện, nhưng đến một lúc nào đó, bạn vẫn phải đợi tệp được đọc để thực hiện điều gì đó với dữ liệu trong tệp. Vì vậy, tốc độ trên luồng chính vẫn khá quan trọng.

Bây giờ, bạn có thực sự muốn sử dụng lệnh gọi lại không?

Ý tôi là, bạn có thực sự muốn không?


Tôi không biết. Tôi chắc chắn không muốn bảo ai phải làm gì.

Nhưng tôi thích cú pháp rõ ràng của async/awaits.


Họ trông đẹp hơn.


Họ đọc tốt hơn.


Tôi biết rõ hơn ở đây là chủ quan nhưng tôi nhớ địa ngục gọi lại và tôi rất biết ơn khi những lời hứa xuất hiện. Nó làm cho Javascript có thể chấp nhận được.


Giờ đây, Golang rõ ràng nhanh hơn Node.js ở mức tối ưu, với các lệnh gọi lại và với async/await, gấp 9,2 lần! Vì vậy, nếu chúng ta muốn khả năng đọc và hiệu suất tốt, Golang là người chiến thắng. Mặc dù vậy, tôi rất muốn tìm hiểu xem Golang trông như thế nào.


Dù sao đi nữa. Điều này thật thú vị. Đây giống như một bài tập giúp tôi hiểu cách hoạt động của lệnh gọi lại và I/O trong Vòng lặp sự kiện.

Vì vậy, để đăng xuất

Node.js có chậm không? Hay chúng ta chỉ đang sử dụng Node.js ở chế độ chậm?


Có lẽ ở chỗ hiệu suất là vấn đề quan trọng, Golang đáng để nhảy vào. Tôi chắc chắn sẽ xem xét sử dụng Golang nhiều hơn trong tương lai.


Cũng xuất hiện ở đây .