个性化阅读
专注于IT技术分析

对Node.js Promise进行基准测试

本文概述

我们生活在一个勇敢的新世界中。一个充满JavaScript的世界。近年来, JavaScript主导了整个网络, 席卷了整个行业。在引入Node.js之后, JavaScript社区就能够利用该语言的简单性和动态性, 将其作为唯一的语言来处理所有事情, 包括处理服务器端, 客户端, 甚至大胆地宣称自己可以从事机器学习。但是在过去的几年中, JavaScript作为一种语言已经发生了巨大的变化。引入了从未有过的新概念, 例如箭头功能和Promise。

啊, 保证。当我刚开始学习Node.js时, Promise和回调的整个概念对我来说并没有多大意义。我习惯了执行代码的过程方式, 但是随着时间的推移, 我了解了为什么它很重要。

这带给我们一个问题, 为什么无论如何都要引入回调和promise?为什么我们不能只用JavaScript编写顺序执行的代码?

好吧, 从技术上讲你可以。但是你应该吗?

在本文中, 我将简要介绍一下JavaScript及其运行时, 更重要的是, 测试一下JavaScript界普遍认为同步代码的性能不及标准的, 并且在某种意义上说只是普通的邪恶, 并且永远不要使用。这是真的神话吗?

在开始之前, 本文假定你已经熟悉JavaScript中的Promise, 但是, 如果你不熟悉或需要复习, 请参阅JavaScript Promises:带有示例的教程

N.B.本文已在Node.js环境(而非纯JavaScript环境)上经过测试。运行Node.js版本10.14.2。所有基准测试和语法都将严重依赖于Node.js。测试是在MacBook Pro 2018上运行的, 该处理器采用了Intel i5第八代四核处理器, 基本时钟速度为2.3 GHz。

事件循环

基准化Node.js Promise:Node.js事件循环的图示

编写JavaScript的问题是语言本身是单线程的。这意味着你不能一次执行一个以上的过程, 而与其他语言(例如Go或Ruby)不同, 它们可以在内核线程或进程线程上生成线程并同时执行多个过程。 。

为了执行代码, JavaScript依赖于一个称为事件循环的过程, 该过程由多个阶段组成。 JavaScript过程经历了每个阶段, 最后, 又重新开始。你可以在此处阅读有关node.js官方指南的详细信息。

但是JavaScript可以解决阻塞问题。 I / O回调。

要求我们创建线程的大多数实际用例都是这样的事实, 即我们正在请求某种语言不负责的操作, 例如, 请求从数据库中获取某些数据。在多线程语言中, 创建请求的线程只是挂起或等待数据库的响应。这只是浪费资源。这也给开发人员增加了在线程池中选择正确数量的线程的负担。这是为了防止内存泄漏以及在应用程序需求量很大时分配大量资源。

JavaScript在处理I / O操作方面胜于其他任何方面。 JavaScript使你可以调用I / O操作, 例如从数据库请求数据, 将文件读入内存, 将文件写至磁盘, 执行Shell命令等。操作完成后, 你将执行回调。或者, 在有诺言的情况下, 你可以用结果来解决诺言或以错误拒绝它。

JavaScript社区始终建议我们在进行I / O操作时千万不要使用同步代码。众所周知的原因是我们不想阻止我们的代码运行其他任务。由于它是单线程的, 因此如果我们有一段代码可以同步读取文件, 那么该代码将阻塞整个过程, 直到读取完成。相反, 如果我们依赖异步代码, 则可以执行多个I / O操作, 并在完成每个操作时分别处理每个操作的响应。没有任何阻碍。

但是, 可以肯定的是, 在我们根本不关心处理大量流程的环境中, 使用同步和异步代码根本没有什么不同, 对吧?

基准测试

我们将要进行的测试旨在为我们提供基准, 以测试同步和异步代码的运行速度以及性能是否存在差异。

我决定选择读取文件作为I / O操作进行测试。

首先, 我编写了一个函数, 该函数将编写一个随机文件, 该文件填充有Node.js Crypto模块生成的随机字节。

const fs = require('fs');
const crypto = require('crypto');

fs.writeFileSync( "./test.txt", crypto.randomBytes(2048).toString('base64') )

该文件将作为下一步读取文件的常量。这是代码

const fs = require('fs');

process.on('unhandledRejection', (err)=>{
 console.error(err);
})

function synchronous() {
 console.time("sync");
 fs.readFileSync("./test.txt")
 console.timeEnd("sync")
}

async function asynchronous() {
 console.time("async");
 let p0 = fs.promises.readFile("./test.txt");
 await Promise.all([p0])
 console.timeEnd("async")
}

synchronous()
asynchronous()

运行前面的代码将产生以下结果:

运行# 同步 异步 异步/同步比率
1 0.278毫秒 3, 829毫秒 13.773
2 0.335毫秒 3.801毫秒 11.346
3 0.403毫秒 4.498毫秒 11.161

这是出乎意料的。我最初的期望是他们应该花相同的时间。好吧, 我们如何添加另一个文件并读取2个文件而不是1个文件呢?

我复制了生成的test.txt文件, 并将其命名为test2.txt。这是更新的代码:

function synchronous() {
 console.time("sync");
 fs.readFileSync("./test.txt")
 fs.readFileSync("./test2.txt")
 console.timeEnd("sync")
}

async function asynchronous() {
 console.time("async");
 let p0 = fs.promises.readFile("./test.txt");
 let p1 = fs.promises.readFile("./test2.txt");
 await Promise.all([p0, p1])
 console.timeEnd("async")
}

我只是为它们每个添加了另一个阅读, 并且在诺言中, 我正在等待应该并行运行的阅读诺言。结果是:

运行# 同步 异步 异步/同步比率
1 1.659毫秒 6.895毫秒 4.156
2 0.323毫秒 4.048毫秒 12.533
3 0.324毫秒 4.017毫秒 12.398
4 0.333毫秒 4.271毫秒 12.826

第一个具有与随后的三个运行完全不同的值。我的猜测是, 它与JavaScript JIT编译器有关, 后者可在每次运行时优化代码。

因此, 异步功能看起来不太好。也许, 如果我们使事情变得更加动态, 或者给应用程序施加更多压力, 我们可能会得出不同的结果。

因此, 我的下一个测试涉及写入100个不同的文件, 然后全部读取。

首先, 我修改了代码以在执行测试之前写入100个文件。每次运行时文件都不同, 尽管它们的大小几乎保持相同, 因此我们在每次运行前都要清除旧文件。

这是更新的代码:

let filePaths = [];

function writeFile() {
 let filePath = `./files/${crypto.randomBytes(6).toString('hex')}.txt`
 fs.writeFileSync( filePath, crypto.randomBytes(2048).toString('base64') )
 filePaths.push(filePath);
}

function synchronous() {
 console.time("sync");
 /* fs.readFileSync("./test.txt")
 fs.readFileSync("./test2.txt") */
 filePaths.forEach((filePath)=>{
   fs.readFileSync(filePath)
 })
 console.timeEnd("sync")
}

async function asynchronous() {
 console.time("async");
 /* let p0 = fs.promises.readFile("./test.txt");
 let p1 = fs.promises.readFile("./test2.txt"); */
 // await Promise.all([p0, p1])
 let promiseArray = [];
 filePaths.forEach((filePath)=>{
   promiseArray.push(fs.promises.readFile(filePath))
 })
 await Promise.all(promiseArray)
 console.timeEnd("async")
}

并进行清理和执行:

let oldFiles = fs.readdirSync("./files")
oldFiles.forEach((file)=>{
 fs.unlinkSync("./files/"+file)
})
if (!fs.existsSync("./files")){
 fs.mkdirSync("./files")
}


for (let index = 0; index < 100; index++) {
 writeFile()
}

synchronous()
asynchronous()

让我们开始吧。

这是结果表:

运行# 同步 异步 异步/同步比率
1 4.999毫秒 12.890毫秒 2.579
2 5.077毫秒 16.267毫秒 3.204
3 5.241毫秒 14.571毫秒 2.780
4 5, 086毫秒 16.334毫秒 3.213

这些结果在这里开始得出结论。它表明随着需求或并发性的增加, Promise开销开始变得有意义。详细说来, 如果我们正在运行一个Web服务器, 该服务器应该每秒每服务器运行数百或数千个请求, 那么使用同步运行I / O操作将开始很快失去其优势。

仅出于实验目的, 让我们看看这是否确实是诺言本身的问题, 还是其他原因。为此, 我编写了一个函数, 该函数将计算时间来解决一个绝对不做任何Promise的Promise, 另一个解决100个空Promise的Promise。

这是代码:

function promiseRun() {
 console.time("promise run");
 return new Promise((resolve)=>resolve())
 .then(()=>console.timeEnd("promise run"))
}

function hunderedPromiseRuns() {
 let promiseArray = [];
 console.time("100 promises")
 for(let i = 0; i < 100; i++) {
   promiseArray.push(new Promise((resolve)=>resolve()))
 }
 return Promise.all(promiseArray).then(()=>console.timeEnd("100 promises"))
}

promiseRun()
hunderedPromiseRuns()
运行# 单一Promise 100个Promise
1 1.651毫秒 3.293毫秒
2 0.758毫秒 2.575毫秒
3 0.814毫秒 3.127毫秒
4 0.788毫秒 2.623毫秒

有趣。看来诺言不是造成延迟的主要原因, 这让我猜测延迟的来源是内核线程在进行实际读取。对于延迟的主要原因, 这可能需要更多的实验才能得出决定性的结论。

本文总结

那你应该不使用诺言吗?我的看法如下:

如果你要编写的脚本可以在单台计算机上运行, ​​且管道或单个用户触发了特定的流程, 那么请使用同步代码。如果你要编写一个负责处理大量流量和请求的Web服务器, 则异步执行产生的开销将克服同步代码的性能。

你可以在存储库中找到本文中所有功能的代码。

从Promise开始, JavaScript开发者旅程中的合乎逻辑的下一步就是async / await语法。如果你想详细了解它以及我们如何到达这里, 请参阅异步JavaScript:从回调地狱到异步和等待。

赞(0)
未经允许不得转载:srcmini » 对Node.js Promise进行基准测试

评论 抢沙发

评论前必须登录!