关键代码拆解成如下图所示(无关部分已省略):

起初我认为可能是这个 getRowDataItemNumberFormat 函数里面某些方法执行太慢,从 formatData.replaceunescape(已废弃,官方建议使用 decodeURI 或者 decodeURIComponent 替代) 方法都怀疑了一遍,发现这些方法都不是该函数运行慢的原因。为了深究原因,我给 style.formatData 传入了不同的值,发现这个函数的运行效率出现不同的表现。开始有点疑惑为什么 style.formatData 的值导致这个函数的运行效率差别如此之大。

进一步最终定位发现如果 style.formatData 为 undefined 的时候,效率骤降,如果 style.formatData 为合法的字符串的时候,效率是正常值。我开始意识到这个问题的原因在那里了,把目光转向了 try catch 代码块,这是一个很可疑的地方,在很早之前曾经听说过不合理的 try catch 是会影响性能的,但是之前从没遇到过,结合了一些资料,我发现比较少案例去探究这类代码片段的性能,我决定写代码去验证下:

window.a = 'a';
window.c = undefined;
function getRowDataItemNumberFormatTryCatch() {
console.time('getRowDataItemNumberFormatTryCatch');
for (let i = 0; i < 3000; i++) {
try {
a.replace(/%022/g, '"');
}
catch (error) {
}
}
console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

我尝试把 try catch 放入一个 for 循环中,让它运行 3000 次,看看它的耗时为多少,我的电脑执行该代码的时间大概是 0.2 ms 左右,这是一个比较快的值,但是这里 a.replace 是正常运行的,也就是 a 是一个字符串能正常运行 replace 方法,所以这里的耗时是正常的。我对他稍微做了一下改变,如下:

function getRowDataItemNumberFormatTryCatch2() {
console.time('getRowDataItemNumberFormatTryCatch');
for (let i = 0; i < 3000; i++) {
try {
c.replace(/%022/g, '"');
}
catch (error) {
}
}
console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

这段代码跟上面代码唯一的区别是,c.replace 此时应该是会报错的,因为 cundefined,这个错误会被 try catch 捕捉到,而上面的代码耗时出现了巨大的变化,上升到 40 ms,相差了将近 200 倍!并且上述代码和首图的 getRowDataItemNumberFormat 函数代码均出现了 Minor GC,注意这个 Minor GC 也是会耗时的。

这可以解释一部分原因了,我们上面运行的代码是一个性能比较关键的部分,不应该使用 try catch 结构,因为该结构是相当独特的。与其他构造不同,它运行时会在当前作用域中创建一个新变量。每次 catch 执行该子句都会发生这种情况,将捕获的异常对象分配给一个变量。

即使在同一作用域内,此变量也不存在于脚本的其他部分中。它在 catch 子句的开头创建,然后在子句末尾销毁。因为此变量是在运行时创建和销毁的(这些都需要额外的耗时!),并且这是 JavaScript 语言的一种特殊情况,所以某些浏览器不能非常有效地处理它,并且在捕获异常的情况下,将捕获处理程序放在性能关键的循环中可能会导致性能问题,这是我们为什么上面会出现 Minor GC 并且会有严重耗时的原因。

如果可能,应在代码中的较高级别上进行异常处理,在这种情况下,异常处理可能不会那么频繁发生,或者可以通过首先检查是否允许所需的操作来避免。上面的 getRowDataItemNumberFormatTryCatch2 函数示例显示的循环,如果里面所需的属性不存在,则该循环可能引发多个异常,为此性能更优的写法应该如下:

function getRowDataItemNumberFormatIf() {
console.time('getRowDataItemNumberFormatIf');
for (let i = 0; i < 3000; i++) {
if (c) {
c.replace(/%022/g, '"');
}
}
console.timeEnd('getRowDataItemNumberFormatIf')
}

上面的这段代码语义上跟 try catch 其实是相似的,但运行效率迅速下降至 0.04ms,所以 try catch 应该通过检查属性或使用其他适当的单元测试来完全避免使用此构造,因为这些构造会极大地影响性能,因此应尽量减少使用它们。

如果一个函数被重复调用,或者一个循环被重复求值,那么最好避免其中包含这些构造。它们最适合仅执行一次或仅执行几次且不在性能关键代码内执行的代码。尽可能将它们与其他代码隔离,以免影响其性能。

例如,可以将它们放在顶级函数中,或者运行它们一次并存储结果,这样你以后就可以再次使用结果而不必重新运行代码。

getRowDataItemNumberFormat 在经过上述思路改造后,运行效率得到了质的提升,在实测 300 多次循环中减少的时间如下图,足足优化了将近 2s 多的时间,如果是 3000 次的循环,那么它的优化比例会更高:



由于上面的代码是从项目中改造出来演示的,可能并不够直观,所以我重新写了另外一个相似的例子,代码如下,这里面的逻辑和上面的 getRowDataItemNumberFormat 函数讲道理是一致的,但是我让其发生错误的时候进入 catch 逻辑执行任务。

事实上 plus1plus2 函数的代码逻辑是一致的,只有代码语义是不相同,一个是返回 1,另一个是错误抛出1,一个求和方法在 try 片段完成,另一个求和方法再 catch 完成,我们可以粘贴这段代码在浏览器分别去掉不同的注释观察结果。

我们发现 try 片段中的代码运行大约使用了 0.1 ms,而 catch 完成同一个求和逻辑却执行了大约 6 ms,这符合我们上面代码观察的预期,如果把计算范围继续加大,那么这个差距将会更加明显,实测如果计算 300000 次,那么将会由原来的 60 倍差距扩大到 500 倍,那就是说我们执行的 catch 次数越少折损效率越少,而如果我们执行的 catch 次数越多那么折损的效率也会越多。

所以在不得已的情况下使用 try catch 代码块,也要尽量保证少进入到 catch 控制流分支中。

const plus1 = () => 1;
const plus2 = () => { throw 1 };
console.time('sum');
let sum = 0;
for (let i = 0; i < 3000; i++) {
try {
// sum += plus1(); // 正确时候 约 0.1ms
sum += plus2(); // 错误时候 约 6ms
} catch (error) {
sum += error;
}
}
console.timeEnd('sum');

上面的种种表现进一步引发了我对项目性能的一些思考,我搜了下我们这个项目至少存在 800 多个 try catch,糟糕的是我们无法保证所有的 try catch 是不损害代码性能并且有意义的,这里面肯定会隐藏着很多上述类的 try catch 代码块。

从性能的角度来看,目前 V8 引擎确实在积极的通过 try catch 来优化这类代码片段,在以前浏览器版本中上面整个循环即使发生在 try catch 代码块内,它的速度也会变慢,因为以前浏览器版本会默认禁用 try catch 内代码的优化来方便我们调试异常。

try catch 需要遍历某种结构来查找 catch 处理代码,并且通常以某种方式分配异常(例如:需要检查堆栈,查看堆信息,执行分支和回收堆栈)。尽管现在大部分浏览器已经优化了,我们也尽量要避免去写出上面相似的代码,比如以下代码:

try {
container.innerHTML = "I'm alloyteam";
}
catch (error) {
// todo
}

上面这类代码我个人更建议写成如下形式,如果你实际上抛出并捕获了一个异常,它可能会变慢,但是由于在大多数情况下上面的代码是没有异常的,因此整体结果会比异常更快。

这是因为代码控制流中没有分支会降低运行速度,换句话说就是这个代码执行没错误的时候,没有在 catch 中浪费你的代码执行时间,我们不应该编写过多的 try catch 这会在我们维护和检查代码的时候提升不必要的成本,有可能分散并浪费我们的注意力。

当我们预感代码片段有可能出错,更应该是集中注意力去处理 successerror 的场景,而非使用 try catch 来保护我们的代码,更多时候 try catch 反而会让我们忽略了代码存在的致命问题。

if (container) container.innerHTML = "I'm alloyteam";
else // todo

在简单代码中应当减少甚至不用 try catch ,我们可以优先考虑 if else 代替,在某些复杂不可测的代码中也应该减少 try catch(比如异步代码),我们看过很多 asyncawait 的示例代码都是结合 try catch 的,在很多性能场景下我认为它并不合理,个人觉得下面的写法应该是更干净,整洁和高效的。

因为 JavaScript 是事件驱动的,虽然一个错误不会停止整个脚本,但如果发生任何错误,它都会出错,捕获和处理该错误几乎没有任何好处,代码主要部分中的 try catch 代码块是无法捕获事件回调中发生的错误。

通常更合理的做法是在回调方法通过第一个参数传递错误信息,或者考虑使用 Promisereject() 来进行处理,也可以参考 node 中的常见写法如下:

;(async () => {
const [err, data] = await readFile();
if (err) {
// todo
};
})() fs.readFile('<directory>', (err, data) => {
if (err) {
// todo
}
});

结合了上面的一些分析,我自己做出一些浅显的总结:

    1. 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用 try catch 来捕获异常。
    1. 非异常路径不需要额外的 try catch,确保异常路径在需要考虑性能情况下优先考虑 if else,不考虑性能情况请君随意,而异步可以考虑回调函数返回 error 信息对其处理或者使用 Promse.reject()
    1. 应当适当减少 try catch 使用,也不要用它来保护我们的代码,其可读性和可维护性都不高,当你期望代码是异常时候,不满足上述1,2的情景时候可考虑使用。

最后,笔者希望这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!附笔记链接,阅读往期更多优质文章可移步查看,喜欢的可以给我点赞鼓励哦:https://github.com/Wscats/CV/issues/33

try catch引发的性能优化深度思考的更多相关文章

  1. Android app 性能优化的思考--性能卡顿不好的原因在哪?

    说到 Android 系统手机,大部分人的印象是用了一段时间就变得有点卡顿,有些程序在运行期间莫名其妙的出现崩溃,打开系统文件夹一看,发现多了很多文件,然后用手机管家 APP 不断地进行清理优化 ,才 ...

  2. Android APP 性能优化的一些思考

    说到 Android 系统手机,大部分人的印象是用了一段时间就变得有点卡顿,有些程序在运行期间莫名其妙的出现崩溃,打开系统文件夹一看,发现多了很多文件,然后用手机管家 APP 不断地进行清理优化 ,才 ...

  3. MySQL 5.7 优化SQL提升100倍执行效率的深度思考(GO)

    系统环境:微软云Linux DS12系列.Centos6.5 .MySQL 5.7.10.生产环境,step1,step2是案例,精彩的剖析部分在step3,step4. 1.慢sql语句大概需要13 ...

  4. CUDA性能优化----warp深度解析

    本文转自:http://blog.163.com/wujiaxing009@126/blog/static/71988399201701224540201/ 1.引言 CUDA性能优化----sp, ...

  5. T- SQL性能优化详解

    摘自:http://www.cnblogs.com/Shaina/archive/2012/04/22/2464576.html 故事开篇:你和你的团队经过不懈努力,终于使网站成功上线,刚开始时,注册 ...

  6. SQL Server 性能优化详解

    故事开篇:你和你的团队经过不懈努力,终于使网站成功上线,刚开始时,注册用户较少,网站性能表现不错,但随着注册用户的增多,访问速度开始变慢,一些用户开始发来邮件表示抗议,事情变得越来越糟,为了留住用户, ...

  7. Web性能优化系列:10个JavaScript性能提升的技巧

    由 伯乐在线 - Delostik 翻译,黄利民 校稿.未经许可,禁止转载!英文出处:jonraasch.com.欢迎加入翻译小组. Nicholas Zakas是一位 JS 大师,Yahoo! 首页 ...

  8. Unity 性能优化(力荐)

    开始之前先分享几款性能优化的插件: 1.SimpleLOD : 除了同样拥有Mesh Baker所具有的Mesh合并.Atlas烘焙等功能,它还能提供Mesh的简化,并对动态蒙皮网格进行了很好的支持. ...

  9. Quick BI的复杂系统为例:那些年,我们一起做过的性能优化

    背景 一直以来,性能都是技术层面不可避开的话题,尤其在中大型复杂项目中.犹如汽车整车性能,追求极速的同时,还要保障舒适性和实用性,而在汽车制造的每个环节.零件整合情况.发动机调校等等,都会最终影响用户 ...

随机推荐

  1. 【MySQL】MySQL(三)存储过程和函数、触发器、事务

    MySQL存储过程和函数 存储过程和函数的概念 存储过程和函数是 事先经过编译并存储在数据库中的一段 SQL 语句的集合 存储过程和函数的好处 存储过程和函数可以重复使用,减轻开发人员的工作量.类似于 ...

  2. Java(5)输入和输出

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201515.html 博客主页:https://www.cnblogs.com/testero ...

  3. xpath helper插件安装提示程序包无效

    参考链接:https://www.jianshu.com/p/b7d782ef81e0 刚学到爬虫,需要在Chrome浏览器安装xpath helper插件结果一直提示"程序包无效" ...

  4. .NET CLI简单教程和项目结构

    WHAT IS .NET CLI ? .NET 命令行接口 (CLI) 工具是用于开发.生成.运行和发布 .NET 应用程序的跨平台工具链. 来源:.NET CLI | Microsoft Docs ...

  5. 乘风破浪,遇见上一代操作系统Windows 10 - 抢鲜尝试安装新微软商店(Microsoft Store)

    背景 在微软官方文章的<十一项关于微软商店新知>中提到: 新的微软商店现在可在Windows 11上找到,我们很高兴地分享,它将在未来几个月内提供给Windows 10客户!我们将很快分享 ...

  6. python3 调用 centos 常用系统命令

    一.创建目录 1 import os 2 3 base_path = '/data/sw_backup' 4 addr= 'FT' 5 ip='192.168.1.1' 6 path = base_p ...

  7. JuiceFS CSI Driver 的最佳实践

    文章根据 Juicedata 工程师朱唯唯,在云原生 Meetup 杭州站所作主题演讲<JuiceFS CSI Driver 的最佳实践>整理而成. 大家好,我是来自 Juicedata ...

  8. python解释器下载安装指导

    一.python解释器下载 想要通关python这项语言与计算机进行沟通,我们就必须下载一款能让计算机理解python这项语言的解释器,这时候我们就需要到网上下一个python解释器. python解 ...

  9. Iceberg概述

    背景 随着大数据领域的不断发展, 越来越多的概念被提出并应用到生产中而数据湖概念就是其中之一, 其概念参照阿里云的简介: 数据湖是一个集中式存储库, 可存储任意规模结构化和非结构化数据, 支持大数据和 ...

  10. redux 的简单实用案例

    redux 的简单实用案例 整体思想与结构 创建一个Action 创建一个Reducer 创建Store 在App组件开始使用 整体思想与结构 文件目录如下: 构建 action,通过创建一个函数,然 ...