60 亿次 for 循环,原来这么多东西
起因 有人在思否论坛上向我付费提问 当时觉得,这个人问的有问题吧。仔细一看,还是有点东西的 问题重现 编写一段 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启动服务,用 time curl调用这个接口 首次需要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(`Thescriptusesapproximately${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(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end('Hello'+num);}).listen(8888); 测试结果: 内存占用和 CPU都正常 跟字符串拼接有关,此刻关闭字符串拼接(此时为了快速测试,我把循环次数降到 5.9亿次) 发现耗时稳定下来了 定位问题在字符串拼接,先看看字符串拼接的几种方式 一、使用连接符 “+” 把要连接的字符串连起来 vara='java'varb=a+'script' * 只连接100个以下的字符串建议用这种方法最方便 二、使用数组的 join 方法连接字符串 vararr=['hello','java','script']varstr=arr.join("") 比第一种消耗更少的资源,速度也更快 三、使用模板字符串,以反引号( ` )标识 vara='java'varb=`hello${a}script` 四、使用 JavaScript concat() 方法连接字符串 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');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<590000000;i++){num+=i;}constarr=['Hello'];arr.push(num);console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${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(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<5900000000;i++){//num++;}constarr=['Hello'];//arr[1]=num;console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end('hello');}).listen(8888); 测试结果: 现在我怀疑,不仅仅是字符串拼接的效率问题,更重要的是 for循环的耗时不一致 varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<5900000000;i++){//num++;}console.timeEnd('测试');constarr=['Hello'];//arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${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循环: varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<600000;i++){num++;for(letj=0;j<10000;j++){num++;}}console.timeEnd('测试');constarr=['Hello'];console.log(num,'num');arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${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(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<600000;i++){num++;for(letj=0;j<10000;j++){num++;}}console.timeEnd('测试');//constarr=['Hello'];console.log(num,'num');//arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${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(leti=1;i<60000;i++){num++;for(letj=0;j<100000;j++){num++;}} 只要 num到 60亿即可,解决了这个问题。 哪些场景会遇到这个类似的超大计算量问题: 图片处理 加解密 ❝ 如果是异步的业务场景,也可以用多进程参与解决超大计算量问题,今天这里就不重复介绍了 ❞ 最后 如果感觉写得不错,可以点个 在看/ 赞,转发一下,让更多人看到 我是 Peter谭老师,欢迎你关注公众号: 前端巅峰,后台回复: 加群即可加入大前端交流群 本文分享自微信公众号 - 前端巅峰(Java-Script-)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。