IntelliJ IDEA C语言 深度图像 docker安装 kubernetes encoding oauth loam算法测试 flexbox static jqgrid Animsition vue开发文档 vue网页 后台管理ui photoshop视频教程全集下载 pmp教学视频 wps临时文件 idea整理代码格式 pip环境变量配置 vue使用bootstrap python中index的用法 python调用自定义函数 java基础学习 java连接mysql数据库 java中的数据结构 java字符串替换 java时间转时间戳 java游戏开发 嵌入式开发教程 黑客攻防实战入门 离散数学及其应用 java电子书下载 打马赛克的软件 苹果滚动截屏 动态加载js 计价软件 fireworks8序列号 刷新页面 js刷新当前页
当前位置: 首页 > 学习教程  > 编程语言

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

2020/8/31 15:33:08 文章标签:

起因

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

image.png

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

问题重现

  • 编写一段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调用这个接口

image.png

  • 首次需要7.xxs耗时
  • 多次调用后,问题重现

image.png

  • 为什么这个耗时突然变高,由于我是调用的是本机服务,我看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);
  • 测试结果:

image.png

  • 内存占用和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亿次循环)

image.png

  • 《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);
  • 测试运行结果:

image.png

  • 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);
  • 测试结果稳定,符合预期:

image.png

总结:

  • 对于单次循环超过一定阀值次数的,用拆解方式,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亿即可,解决了这个问题。

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

  • 图片处理
  • 加解密

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

感谢阅读

给大家安利一款分享学习资料的app,此app的主要就是收集了互联网上的各种学习资料做整合并免费分享。里面涵盖了Android、java、python、数据结构、算法、前端、爬虫、大数据、深度学习、UI等等技术,包含某课、某课堂VIP课程、各培训机构课程、某鱼的一些付费课程,并且正在不断更新中,你想要的学习资料几乎都有,免费获取超5T资料以及面试资料,当然你也可以加入我们的技术Q群获取资料:687667883
如果你想先了解有哪些课程,可查看此链接:免费获取IT学习资料课程
download地址如下:
在这里插入图片描述


本文链接: http://www.dtmao.cc/news_show_150356.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?