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

tech2025-02-01  11

起因

有人在思否论坛上向我付费提问

当时觉得,这个人问的有问题吧。仔细一看,还是有点东西的

问题重现

编写一段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亿即可,解决了这个问题。

哪些场景会遇到这个类似的超大计算量问题:

图片处理

加解密

如果是异步的业务场景,也可以用多进程参与解决超大计算量问题,今天这里就不重复介绍了

推荐阅读

1、力扣刷题插件

2、你不知道的 TypeScript 泛型(万字长文,建议收藏)

3、TypeScript 类型系统

4、immutablejs 是如何优化我们的代码的?

5、typeScript 配置文件该怎么写?

6、前端换肤的N种方案,请收下

7、【校招面经分享】好未来-北京-视频面试

关注加加,星标加加~

如果觉得文章不错,帮忙点个在看呗

最新回复(0)