起因
- 有人在思否論壇上向我付費提問
- 當時覺得,這個人問的有問題吧。仔細一看,還是有點東西的
問題重現
- 編寫一段
Node.js
程式碼
var http = require('http');
http.createServer(function (request, response) {
var num = 0
for (var i = 1; i < 5900000000; i++) {
num += i
}
response.end('Hello' + num);
}).listen(8888);
- 使用
nodemon
啟動服務,用time curl
呼叫這個介面
- 首次需要
7.xxs
耗時 - 多次呼叫後,問題重現
- 為什麼這個耗時突然變高,由於我是呼叫的是本機服務,我看
CPU
使用當時很高,差不多打到100%
了.但是我後面發現不是這個問題.
問題排查
- 排除掉
CPU
問題,看記憶體消耗佔用。
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
console.time('測試');
let num = 0;
for (let i = 1; i < 5900000000; i++) {
num += i;
}
console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end('Hello' + num);
![](https://imgkr2.cn-bj.ufileos.com/13455121-9d87-42c3-a32e-ea999a2cd09b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=E3cF2kymC92LifrIC5IOfIZQvnk%253D&Expires=1598883364)
![](https://imgkr2.cn-bj.ufileos.com/1e7b95df-2a48-41c3-827c-3c24b39f4b5b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=%252FANTTuhgbpIsXslXMc1qCkj2TMU%253D&Expires=1598883362)
})
.listen(8888);
- 測試結果:
- 記憶體佔用和
CPU
都正常 - 跟字串拼接有關,此刻關閉字串拼接(此時為了快速測試,我把迴圈次數降到
5.9億次
)
- 發現耗時穩定下來了
定位問題在字串拼接,先看看字串拼接的幾種方式
- 一、使用連線符 “+” 把要連線的字串連起來
var a = 'java'
var b = a + 'script'
* 只連線100個以下的字串建議用這種方法最方便
- 二、使用陣列的 join 方法連線字串
var arr = ['hello','java','script']
var str = arr.join("")
- 比第一種消耗更少的資源,速度也更快
- 三、使用模板字串,以反引號( ` )標識
var a = 'java'
var b = `hello ${a}script`
- 四、使用 JavaScript concat() 方法連線字串
var a = 'java'
var b = 'script'
var str = a.concat(b)
五、使用物件屬性來連線字串
function StringConnect(){
this.arr = new Array()
}
StringConnect.prototype.append = function(str) {
this.arr.push(str)
}
StringConnect.prototype.toString = function() {
return this.arr.join("")
}
var mystr = new StringConnect()
mystr.append("abc")
mystr.append("def")
mystr.append("g")
var str = mystr.toString()
更換字串的拼接方式
- 我把字串拼接換成了陣列的
join
方式(此時迴圈5.9
億次)
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
console.time('測試');
let num = 0;
for (let i = 1; i < 590000000; i++) {
num += i;
}
const arr = ['Hello'];
arr.push(num);
console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end(arr.join(''));
})
.listen(8888);
- 測試結果,發現介面呼叫的耗時穩定了(
注意此時是5.9億次迴圈
)
《javascript高階程式設計》
中,有一段關於字串特點的描述,原文大概如下:ECMAScript
中的字串是不可變的,也就是說,字串一旦建立,他們的值就不能改變。要改變某個變數的儲存的的字串,首先要銷燬原來的字串,然後再用另外一個包含新值的字串填充該變數
就完了?
- 用
+
直接拼接字串自然會對效能產生一些影響,因為字串是不可變的,在操作的時候會產生臨時字串副本,+
操作符需要消耗時間,重新賦值分配記憶體需要消耗時間。 - 但是,我更換了程式碼後,發現,即使沒有字串拼接,也會耗時不穩定
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
console.time('測試');
let num = 0;
for (let i = 1; i < 5900000000; i++) {
// num++;
}
const arr = ['Hello'];
// arr[1] = num;
console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end('hello');
})
.listen(8888);
- 測試結果:
- 現在我懷疑,不僅僅是字串拼接的效率問題,更重要的是
for
迴圈的耗時不一致
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
let num = 0;
console.time('測試');
for (let i = 1; i < 5900000000; i++) {
// num++;
}
console.timeEnd('測試');
const arr = ['Hello'];
// arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end('hello');
})
.listen(8888);
- 測試執行結果:
for
迴圈內部的i++
其實就是變數不斷的重新賦值覆蓋- 經過我的測試發現,
40億次
跟50億次
的區別,差距很大,40億次的for迴圈
,都是穩定的,但是50億次
就不穩定了. Node.js
的EventLoop
:- 我們目前被阻塞的狀態:
- 我電腦的
CPU
使用情況
優化方案
- 遇到了
60億
次的迴圈,像有使用多程式非同步計算的,但是本質上沒有解決這部分迴圈程式碼的呼叫耗時。 - 改變策略,拆解單次次數過大的
for
迴圈:
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
let num = 0;
console.time('測試');
for (let i = 1; i < 600000; i++) {
num++;
for (let j = 0; j < 10000; j++) {
num++;
}
}
console.timeEnd('測試');
const arr = ['Hello'];
console.log(num, 'num');
arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end(arr.join(''));
})
.listen(8888);
- 結果,耗時基本穩定,
60億次
迴圈總共:
推翻字串的拼接耗時說法
- 修改程式碼回最原始的
+
方式拼接字串
var http = require('http');
http
.createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
let num = 0;
console.time('測試');
for (let i = 1; i < 600000; i++) {
num++;
for (let j = 0; j < 10000; j++) {
num++;
}
}
console.timeEnd('測試');
// const arr = ['Hello'];
console.log(num, 'num');
// arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
);
response.end(`Hello` + num);
})
.listen(8888);
- 測試結果穩定,符合預期:
總結:
- 對於單次迴圈超過一定閥值次數的,用拆解方式,
Node.js
的執行耗時是穩定,但是如果是迴圈次數過多,那麼就會出現剛才那種情況,阻塞嚴重,耗時不一樣。 - 為什麼?
深度分析問題
- 遍歷60億次,這個數字是有一些大了,如果是40億次,是穩定的
- 這裡應該還是跟
CPU
有一些關係,因為top
檢視一直是在升高 - 此處雖然不是真正意義上的記憶體洩漏,但是我們如果在一個迴圈中不僅要不斷更新
i
的值到60億
,還要不斷更新num
的值60億
,記憶體使用會不斷上升,最終出現兩份60億
的資料,然後再回收。(因為GC自動垃圾回收,一樣會阻塞主執行緒
,多次介面呼叫後,CPU
佔用也會升高) - 使用
for
迴圈拆解後:
for (let i = 1; i < 60000; i++) {
num++;
for (let j = 0; j < 100000; j++) {
num++;
}
}
- 只要
num
到60億
即可,解決了這個問題。
哪些場景會遇到這個類似的超大計算量問題:
- 圖片處理
- 加解密
如果是非同步的業務場景,也可以用多程式參與解決超大計算量問題,今天這裡就不重複介紹了
最後
- 如果感覺寫得不錯,可以點個
在看
/贊
,轉發一下,讓更多人看到 - 我是
Peter譚老師
,歡迎你關注公眾號:前端巔峰
,後臺回覆:加群
即可加入大前端交流群