淺談異步——性能

近非常喜歡用 Promise 或者 Async/Await 的異步編程方式。但最近突然發現一個問題——性能。

我直接說結論,Promise 比普通回調函數慢得多的多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const middle = (res) => { res(1 + 1) }
console.time('Promise')
for (let i = 0; i < 999999; ++i ) {
new Promise(middle)
}
console.timeEnd('Promise') // Promise: 1768.754ms
const testFn = function () {
middle(function (result) { })
}
console.time('function')
for (let i = 0; i < 999999; ++i) {
testFn()
}
console.timeEnd('function') // function: 51.657ms

Promise 的速度相對於普通函數慢了那麼多,是否還應該繼續使用?

眾所周知,Node.js 的優勢之一就是高並發,這是 JavaScript 引擎的異步輪循機制帶來的好處。我們也知道,通常異步操作是解決 I/O 的問題(不至於整個線程卡死),上面的測試用例應該說是純計算的問題,所以測試並不完全。

那麼 I/O 問題的測試,下面來運行一段一個簡單網站(Promise 和回調函數)的測試代碼。

Promise 的測試代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Promise
const express = require('express')
const path = require('path')
// 生成隨機字符串相關
const trueOrFalse = () => Math.round(Math.random()),
backCode = () => 65 + Math.round(Math.random() * 25),
randomChar = (lower = 0) => String.fromCharCode(backCode() + (lower && 32)),
randomString = (length, lower = 0) => randomChar(lower && trueOrFalse()) + (--length ? randomString(length, lower) : '');
const app = express()
let count = 0;
const fsp = require('fs-promise')
app.use((req, res, next) => {
let filepath = path.join(__dirname, '/testdir', String(count++))
fsp.writeFile(filepath, randomString(4096))
.then(() => fsp.unlink(filepath))
.catch(err => next(err))
.then(() => res.end('done: ' + count))
})
app.listen(3333)

普通回調函數的測試代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 普通回調函數
const express = require('express')
const path = require('path')
// 生成隨機字符串相關
const trueOrFalse = () => Math.round(Math.random()),
backCode = () => 65 + Math.round(Math.random() * 25),
randomChar = (lower = 0) => String.fromCharCode(backCode() + (lower && 32)),
randomString = (length, lower = 0) => randomChar(lower && trueOrFalse()) + (--length ? randomString(length, lower) : '');
const app = express()
let count = 0;
const fs = require('fs')
app.use((req, res, next) => {
let filepath = path.join(__dirname, '/testdir', String(count++))
fs.writeFile(filepath, randomString(4096), (err, data) => {
if (err) { return next(err) }
fs.unlink(filepath, err => {
if (err) next(err)
else res.end('done: ' + count)
})
})
})
app.listen(3333)

好的,代碼寫好了,接下來使用 ab[1] 進行測試。分別運行三次測試命令,取最後一個結果。

Promise 的測試結果

1
2
3
4
5
6
7
8
9
10
11
12
13
H:\Software\phpStudy2016\Apache\bin>ab -n 2000 -c 2000 http://localhost:3333/
(省略)
Percentage of the requests served within a certain time (ms)
50% 6572
66% 7076
75% 7172
80% 7176
90% 7581
95% 7972
98% 8157
99% 8160
100% 8171 (longest request)

普通函數的測試結果

1
2
3
4
5
6
7
8
9
10
11
12
13
H:\Software\phpStudy2016\Apache\bin>ab -n 2000 -c 2000 http://localhost:3333/
(省略)
Percentage of the requests served within a certain time (ms)
50% 5993
66% 6383
75% 7643
80% 7647
90% 8130
95% 8141
98% 8144
99% 8145
100% 8261 (longest request)

我們可以看到,兩者差距並不大,因為都在等待 I/O 操作,速度也快不上來。

最終結論

按照第一個測試來看,如果是純運算場景的情況下,建議少用 Promise。但其他情況的影響並不太大,所以在開發的過程中根據運算情況來權衡一下才是較好的實踐。


  1. 好像叫 Apache Benchmark ↩︎