关于javascript:60亿次for循环原来这么多东西

2次阅读

共计 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.jsEventLoop:

  • 咱们目前被阻塞的状态:

  • 我电脑的 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++;}
    }
  • 只有 num60 亿 即可, 解决了这个问题。

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

  • 图片解决
  • 加解密

如果是异步的业务场景,也能够用多过程参加解决超大计算量问题,明天这里就不反复介绍了

最初

  • 如果感觉写得不错,能够点个 在看 /, 转发一下,让更多人看到
  • 我是 Peter 谭老师, 欢送你关注公众号: 前端巅峰 ,后盾回复: 加群 即可退出大前端交换群
正文完
 0