当前位置: 首页 > 后端技术 > Node.js

60亿个for循环,原来这么多东西

时间:2023-04-04 00:26:24 Node.js

是因为有人付钱让我去四福论坛提问。当时我以为这个人问了一个问题。仔细一看,还是有什么问题。复现一段Node.js代码varhttp=require('http');http.createServer(function(request,response){varnum=0for(vari=1;i<5900000000;i++){num+=i}response.end('Hello'+num);}).listen(8888);使用nodemon启动服务,第一次使用timecurl调用该接口,耗时7.xxs,耗时多次,调用后问题重现。为什么耗时突然变高了?由于调用的是本地服务,当时看到CPU占用率非常高,几乎达到100%。但是后来发现不是这个问题。故障排除和故障排除对于CPU问题,看内存消耗。varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用大约${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<5900000000;i++){num+=i;}console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本大约使用${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亿times)发现耗时稳定了,定位问题在字符串拼接。首先,让我们看一下字符串拼接的几种方式。1.使用连接符“+”连接要连接的字符串vara='java'varb=a+'script'*只连接少于100个字符串。建议使用此方法作为最方便的方法。2、使用数组的join方法连接字符串vararr=['hello','java','script']varstr=arr.join("")比第一种消耗资源更少,速度更快3.使用模板字符串,用反引号(`)标记vara='java'varb=`hello${a}script`四、使用JavaScriptconcat()方法连接字符串vara='java'varb='script'varstr=a.concat(b)五、使用对象属性连接字符串functionStringConnect(){this.arr=newArray()}StringConnect.prototype.append=function(str){this.arr.push(str)}StringConnect.prototype.toString=function(){returnthis.arr.join("")}varmystr=newStringConnect()mystr.append("abc")mystr.append("def")mystr.append("g")varstr=mystr.toString()改变字符串的拼接方式我拼接的字符串换成数组的join方式(此时循环5.9亿)varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');让used=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本大约使用${Math.round(used*100)/100}MB`,'start',);console.time('测试');让数=0;for(leti=1;i<590000000;i++){num+=i;}constarr=['你好'];到。推(数);console.timeEnd('测试');使用=process.memoryUsage().heapUsed/1024/1024;console.log(`该脚本大约使用${Math.round(used*100)/100}MB`,'end',);response.end(arr.join(''));}).listen(8888);测试的结果,发现接口调用的耗时是稳定的(注意此时是5.9亿次循环)《javascript高级程序设计》,里面有关于字符串特性的说明。原文大致如下:ECMAScript中的字符串是不可变的,即字符串一旦创建,其值就不能改变。更改变量要保存字符串,首先销毁原始字符串,然后用另一个包含新值的字符串填充变量?使用+直接拼接字符串自然会对性能产生一些影响,因为字符串是不可变的,在操作的时候会产生临时的字符串副本。+运算符需要时间,重新分配和分配内存也需要时间。但是,我替换代码后发现,即使不进行字符串拼接,也会耗时且不稳定varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用大约${Math.round(used*100)/100}MB`,'start',);console.time('Test');letnum=0;for(leti=1;i<5900000000;i++){//num++;}constarr=['Hello'];//arr[1]=num;console.timeEnd('test');used=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用大约${Math.round(used*100)/100}MB`,'结束',);response.end('你好');}).listen(8888);测试结果:现在我怀疑不仅仅是字符串拼接的效率问题,更重要的是for循环的耗时不一致varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用approximat${Math.round(used*100)/100}MB`,'开始',);让数=0;console.time('测试');for(leti=1;i<5900000000;i++){//num++;}console.timeEnd('测试');constarr=['你好'];//arr[1]=num;使用=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本大约使用${Math.round(used*100)/100}MB`,'end',);response.end('你好');}).listen(8888);试运行结果:for循环里面的i++其实就是变量的不断重新赋值。经过我的测试,我发现40亿次和50亿次相差很大。for循环40亿次是稳定的,50亿次就不稳定了。稳定。Node.js的EventLoop:我们目前被封杀:我电脑的CPU使用率优化方案遇到了60亿次循环,看似使用了多进程异步计算,但本质上这部分循环代码一直没有解决时间——消费电话改变策略,反汇编单次次数过多的for循环:varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用大约${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('test');for(leti=1;i<600000;i++){num++;for(letj=0;j<10000;j++){num++;}}console.timeEnd('test');constarr=['Hello'];console.log(num,'num');arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本使用大约${Math.round(used*100)/100}MB`,'end',);response.end(arr.join(''));}).listen(8888);结果耗时基本稳定,一共60亿次循环:推翻字符串拼接耗时语句修改代码回到最原始的+字符串拼接方式varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`脚本大约使用${Math.round(used*100)/100}MB`,'start',);让数=0;console.time('测试');for(让i=1;i<600000;i++){num++;对于(设j=0;j<10000;j++){num++;}}console.timeEnd('测试');//constarr=['你好'];console.log(num,'num');//arr[1]=num;使用=process.memoryUsage().heapUsed/1024/1024;console.log(`该脚本大约使用${Math.round(used*100)/100}MB`,'end',);response.end(`Hello`+num);}).listen(8888);测试结果稳定,符合预期:总结:对于单次循环超过一定阈值的次数,通过拆解Node.js的运行时间是稳定的,但是如果循环次数过多,则出现这种情况刚才会出现,阻塞严重,耗时不同为什么?深入分析问题已遍历60亿次。这个数字有点大。如果是40亿次就稳定了。这个应该跟CPU有关系,因为顶视图这里一直在升,虽然不是真正意义上的。内存泄漏,但是如果我们不仅在循环中不断更新i的值到60亿,还不断更新num的值到60亿,内存占用会不断上升,最终会出现两份6亿数据,然后回收。(因为GC自动垃圾回收也会阻塞主线程,多次接口调用后,CPU占用也会增加)使用for循环反汇编后:for(leti=1;i<60000;i++){num++;对于(设j=0;j<100000;j++){num++;}}只要num达到60亿,这个问题就解决了。哪些场景会遇到这种类似的大规模计算问题:如果图像处理加解密是异步业务场景,也可以采用多进程来解决大规模计算问题。今天在这里就不重复介绍了。最后,如果觉得文笔不错,可以点击观看/点赞,转发,让更多人看到我是PeterTan先生,欢迎大家关注公众号:前端巅峰,后台回复:加群加入前端大交流群