60 亿次 for 循环,原来这么多东西
起因
-
有人在思否论坛上向我付费提问 -
当时觉得,这个人问的有问题吧。仔细一看,还是有点东西的
问题重现
-
编写一段 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亿
即可,解决了这个问题。
哪些场景会遇到这个类似的超大计算量问题:
-
图片处理 -
加解密
❝如果是异步的业务场景,也可以用多进程参与解决超大计算量问题,今天这里就不重复介绍了
❞
最后
-
如果感觉写得不错,可以点个 在看
/赞
,转发一下,让更多人看到 -
我是 Peter谭老师
,欢迎你关注公众号:前端巅峰
,后台回复:加群
即可加入大前端交流群
本文分享自微信公众号 - 前端巅峰(Java-Script-)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
编写高质量可维护的代码:数据建模
👆 这是第 67 篇 不掺水的原创 ,想要了解更多 ,请戳上方蓝色字体: 政采云前端团队 关注我们吧~ 本文首发于政采云前端团队博客:编写高质量可维护的代码:数据建模 https://www.zoo.team/article/data-modeling 什么是数据建模 数据建模是一种用于定义和分析数据的要求和其需要的相应支持的信息系统的过程。 随着前端页面的交互变得更加细腻复杂,原本存放于服务端的状态放置在了前端,类似 flux、redux、mobx、dva、rematch、vuex 的状态管理库也成了每个项目的标配。 因为分层理念的普及,前端工程师们需要把更多精力放在数据管理上,数据建模也成了基本功。 而建模的产物是数据模型,数据模型是定义数据如何输入和输出的一种模型,其主要作用是为信息系统提供数据的定义和格式。 数据模型包括数据结构、数据操作、数据完整性约束条件这三要素。 简单理解就是数据模型提供了一个“模具”,数据按照预先的设计和约束进行放置。 三要素 数据完整性约束条件 好的数据结构必须要有约束,例如描述同一个状态的字段有时候是字符串,有时候是数字,这样的话就容易造成预期之外...
- 下一篇
我工作三年了,该懂并发了(干货)
点击蓝色“程序员cxuan”关注我哟 加个“星标”,欢迎来撩 这是程序员cxuan的第36期分享 本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。 下面我们就来介绍一下 Java 并发中都涉及哪些模块,这些并发模块都是 Java 并发类库所提供的。 同步容器类 同步容器主要包括两类,一种是本来就是线程安全实现的容器,这类容器有 Vector、Hashtable、Stack,这类容器的方法上都加了 synchronized 锁,是线程安全的实现。 “ Vector、Hashtable、Stack 这些容器我们现在几乎都不在使用,因为这些容器在多线程环境下的效率不高。 还有一类是由 Collections.synchronizedxxx 实现的非线程安全的容器,使用 Collections.synchronized 会把它们封装起来编程线程安全的容器,举出两个例子 Collections.synchronizedList Collections.synchronizedMap 我们可以通过 Col...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS6,CentOS7官方镜像安装Oracle11G
- Linux系统CentOS6、CentOS7手动修改IP地址
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8安装Docker,最新的服务器搭配容器使用