原由
-
有人在思否論壇上向我付費提問 -
當時以爲,這我的問的有問題吧。仔細一看,仍是有點東西的
問題重現
-
編寫一段 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
耗時javascript -
屢次調用後,問題重現前端
-
爲何這個耗時忽然變高,因爲我是調用的是本機服務,我看 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億次
)
-
發現耗時穩定下來了
定位問題在字符串拼接,先看看字符串拼接的幾種方式
-
1、使用鏈接符 「+」 把要鏈接的字符串連起來
var a = 'java'
var b = a + 'script'
* 只鏈接100個如下的字符串建議用這種方法最方便java
-
2、使用數組的 join 方法鏈接字符串
var arr = ['hello','java','script']
var str = arr.join("")
-
比第一種消耗更少的資源,速度也更快node
-
3、使用模板字符串,以反引號( ` )標識web
var a = 'java'
var b = `hello ${a}script`
-
4、使用 JavaScript concat() 方法鏈接字符串
var a = 'java'
var b = 'script'
var str = a.concat(b)
5、使用對象屬性來鏈接字符串
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億
便可,解決了這個問題。
哪些場景會遇到這個相似的超大計算量問題:
-
圖片處理 -
加解密
❝若是是異步的業務場景,也能夠用多進程參與解決超大計算量問題,今天這裏就不重複介紹了app
❞
最後
-
若是感受寫得不錯,能夠點個 在看
/贊
,轉發一下,讓更多人看到 -
我是 Peter譚老師
,歡迎你關注公衆號:前端巔峯
,後臺回覆:加羣
便可加入大前端交流羣
本文分享自微信公衆號 - 前端巔峯(Java-Script-)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。curl