60億次for循環(huán),原來這么多東西
起因
有人在思否論壇上向我付費(fèi)提問 
當(dāng)時覺得,這個人問的有問題吧。仔細(xì)一看,還是有點(diǎn)東西的
問題重現(xiàn)
編寫一段 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啟動服務(wù),用time curl調(diào)用這個接口

首次需要
7.xxs耗時多次調(diào)用后,問題重現(xiàn)

為什么這個耗時突然變高,由于我是調(diào)用的是本機(jī)服務(wù),我看 CPU使用當(dāng)時很高,差不多打到100%了.但是我后面發(fā)現(xiàn)不是這個問題.
問題排查
排除掉 CPU問題,看內(nèi)存消耗占用。
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);


??})
??.listen(8888);
測試結(jié)果: 
內(nèi)存占用和 CPU都正常跟字符串拼接有關(guān),此刻關(guān)閉字符串拼接(此時為了快速測試,我把循環(huán)次數(shù)降到 5.9億次)

發(fā)現(xiàn)耗時穩(wěn)定下來了
定位問題在字符串拼接,先看看字符串拼接的幾種方式
一、使用連接符 “+” 把要連接的字符串連起來
var?a?=?'java'
var?b?=?a?+?'script'
* 只連接100個以下的字符串建議用這種方法最方便
二、使用數(shù)組的 join 方法連接字符串
var?arr?=?['hello','java','script']
var?str?=?arr.join("")
比第一種消耗更少的資源,速度也更快
三、使用模板字符串,以反引號( ` )標(biāo)識
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()
更換字符串的拼接方式
我把字符串拼接換成了數(shù)組的 join方式(此時循環(huán)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);
測試結(jié)果,發(fā)現(xiàn)接口調(diào)用的耗時穩(wěn)定了( 注意此時是5.9億次循環(huán))

《javascript高級程序設(shè)計》中,有一段關(guān)于字符串特點(diǎn)的描述,原文大概如下:ECMAScript中的字符串是不可變的,也就是說,字符串一旦創(chuàng)建,他們的值就不能改變。要改變某個變量的保存的的字符串,首先要銷毀原來的字符串,然后再用另外一個包含新值的字符串填充該變量
就完了?
用 +直接拼接字符串自然會對性能產(chǎn)生一些影響,因?yàn)樽址遣豢勺兊?,在操作的時候會產(chǎn)生臨時字符串副本,+操作符需要消耗時間,重新賦值分配內(nèi)存需要消耗時間。但是,我更換了代碼后,發(fā)現(xiàn),即使沒有字符串拼接,也會耗時不穩(wěn)定
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);
測試結(jié)果: 
現(xiàn)在我懷疑,不僅僅是字符串拼接的效率問題,更重要的是 for循環(huán)的耗時不一致
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);
測試運(yùn)行結(jié)果: 
for循環(huán)內(nèi)部的i++其實(shí)就是變量不斷的重新賦值覆蓋經(jīng)過我的測試發(fā)現(xiàn), 40億次跟50億次的區(qū)別,差距很大,40億次的for循環(huán),都是穩(wěn)定的,但是50億次就不穩(wěn)定了.Node.js的EventLoop:

我們目前被阻塞的狀態(tài):

我電腦的
CPU使用情況
優(yōu)化方案
遇到了 60億次的循環(huán),像有使用多進(jìn)程異步計算的,但是本質(zhì)上沒有解決這部分循環(huán)代碼的調(diào)用耗時。改變策略,拆解單次次數(shù)過大的 for循環(huán):
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);
結(jié)果,耗時基本穩(wěn)定, 60億次循環(huán)總共:
推翻字符串的拼接耗時說法
修改代碼回最原始的 +方式拼接字符串
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);
測試結(jié)果穩(wěn)定,符合預(yù)期: 
總結(jié):
對于單次循環(huán)超過一定閥值次數(shù)的,用拆解方式, Node.js的運(yùn)行耗時是穩(wěn)定,但是如果是循環(huán)次數(shù)過多,那么就會出現(xiàn)剛才那種情況,阻塞嚴(yán)重,耗時不一樣。為什么?
深度分析問題
遍歷60億次,這個數(shù)字是有一些大了,如果是40億次,是穩(wěn)定的 這里應(yīng)該還是跟 CPU有一些關(guān)系,因?yàn)?/span>top查看一直是在升高此處雖然不是真正意義上的內(nèi)存泄漏,但是我們?nèi)绻谝粋€循環(huán)中不僅要不斷更新 i的值到60億,還要不斷更新num的值60億,內(nèi)存使用會不斷上升,最終出現(xiàn)兩份60億的數(shù)據(jù),然后再回收。(因?yàn)镚C自動垃圾回收,一樣會阻塞主線程,多次接口調(diào)用后,CPU占用也會升高)使用 for循環(huán)拆解后:
?for?(let?i?=?1;?i?60000;?i++)?{
??????num++;
??????for?(let?j?=?0;?j?100000;?j++)?{
????????num++;
??????}
????}
只要 num到60億即可,解決了這個問題。
哪些場景會遇到這個類似的超大計算量問題:
圖片處理 加解密
?如果是異步的業(yè)務(wù)場景,也可以用多進(jìn)程參與解決超大計算量問題,今天這里就不重復(fù)介紹了
?
推薦閱讀
1、力扣刷題插件
2、你不知道的 TypeScript 泛型(萬字長文,建議收藏)
4、immutablejs 是如何優(yōu)化我們的代碼的?
?關(guān)注加加,星標(biāo)加加~
?
如果覺得文章不錯,幫忙點(diǎn)個在看唄
評論
圖片
表情
