共计 5689 个字符,预计需要花费 15 分钟才能阅读完成。
起因
- 有人在思否论坛上向我付费发问
- 过后感觉,这个人问的有问题吧。认真一看,还是有点货色的
问题重现
- 编写一段
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 谭老师
, 欢送你关注公众号:前端巅峰
,后盾回复:加群
即可退出大前端交换群
正文完
发表至: javascript
2020-08-31