try catch引发的性能优化深度思考
关键代码拆解成如下图所示(无关部分已省略):
起初我认为可能是这个 getRowDataItemNumberFormat
函数里面某些方法执行太慢,从 formatData.replace
到 unescape
(已废弃,官方建议使用 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
此时应该是会报错的,因为 c
是 undefined
,这个错误会被 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
逻辑执行任务。
事实上 plus1
和 plus2
函数的代码逻辑是一致的,只有代码语义是不相同,一个是返回 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
这会在我们维护和检查代码的时候提升不必要的成本,有可能分散并浪费我们的注意力。
当我们预感代码片段有可能出错,更应该是集中注意力去处理 success
和 error
的场景,而非使用 try catch
来保护我们的代码,更多时候 try catch
反而会让我们忽略了代码存在的致命问题。
if (container) container.innerHTML = "I'm alloyteam";
else // todo
在简单代码中应当减少甚至不用 try catch
,我们可以优先考虑 if else
代替,在某些复杂不可测的代码中也应该减少 try catch
(比如异步代码),我们看过很多 async
和 await
的示例代码都是结合 try catch
的,在很多性能场景下我认为它并不合理,个人觉得下面的写法应该是更干净,整洁和高效的。
因为 JavaScript
是事件驱动的,虽然一个错误不会停止整个脚本,但如果发生任何错误,它都会出错,捕获和处理该错误几乎没有任何好处,代码主要部分中的 try catch
代码块是无法捕获事件回调中发生的错误。
通常更合理的做法是在回调方法通过第一个参数传递错误信息,或者考虑使用 Promise
的 reject()
来进行处理,也可以参考 node
中的常见写法如下:
;(async () => {
const [err, data] = await readFile();
if (err) {
// todo
};
})()
fs.readFile('<directory>', (err, data) => {
if (err) {
// todo
}
});
结合了上面的一些分析,我自己做出一些浅显的总结:
- 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用
try catch
来捕获异常。
- 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用
- 非异常路径不需要额外的
try catch
,确保异常路径在需要考虑性能情况下优先考虑if else
,不考虑性能情况请君随意,而异步可以考虑回调函数返回error
信息对其处理或者使用Promse.reject()
。
- 非异常路径不需要额外的
- 应当适当减少
try catch
使用,也不要用它来保护我们的代码,其可读性和可维护性都不高,当你期望代码是异常时候,不满足上述1,2的情景时候可考虑使用。
- 应当适当减少
最后,笔者希望这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!附笔记链接,阅读往期更多优质文章可移步查看,喜欢的可以给我点赞鼓励哦:https://github.com/Wscats/CV/issues/33
try catch引发的性能优化深度思考的更多相关文章
- Android app 性能优化的思考--性能卡顿不好的原因在哪?
说到 Android 系统手机,大部分人的印象是用了一段时间就变得有点卡顿,有些程序在运行期间莫名其妙的出现崩溃,打开系统文件夹一看,发现多了很多文件,然后用手机管家 APP 不断地进行清理优化 ,才 ...
- Android APP 性能优化的一些思考
说到 Android 系统手机,大部分人的印象是用了一段时间就变得有点卡顿,有些程序在运行期间莫名其妙的出现崩溃,打开系统文件夹一看,发现多了很多文件,然后用手机管家 APP 不断地进行清理优化 ,才 ...
- MySQL 5.7 优化SQL提升100倍执行效率的深度思考(GO)
系统环境:微软云Linux DS12系列.Centos6.5 .MySQL 5.7.10.生产环境,step1,step2是案例,精彩的剖析部分在step3,step4. 1.慢sql语句大概需要13 ...
- CUDA性能优化----warp深度解析
本文转自:http://blog.163.com/wujiaxing009@126/blog/static/71988399201701224540201/ 1.引言 CUDA性能优化----sp, ...
- T- SQL性能优化详解
摘自:http://www.cnblogs.com/Shaina/archive/2012/04/22/2464576.html 故事开篇:你和你的团队经过不懈努力,终于使网站成功上线,刚开始时,注册 ...
- SQL Server 性能优化详解
故事开篇:你和你的团队经过不懈努力,终于使网站成功上线,刚开始时,注册用户较少,网站性能表现不错,但随着注册用户的增多,访问速度开始变慢,一些用户开始发来邮件表示抗议,事情变得越来越糟,为了留住用户, ...
- Web性能优化系列:10个JavaScript性能提升的技巧
由 伯乐在线 - Delostik 翻译,黄利民 校稿.未经许可,禁止转载!英文出处:jonraasch.com.欢迎加入翻译小组. Nicholas Zakas是一位 JS 大师,Yahoo! 首页 ...
- Unity 性能优化(力荐)
开始之前先分享几款性能优化的插件: 1.SimpleLOD : 除了同样拥有Mesh Baker所具有的Mesh合并.Atlas烘焙等功能,它还能提供Mesh的简化,并对动态蒙皮网格进行了很好的支持. ...
- Quick BI的复杂系统为例:那些年,我们一起做过的性能优化
背景 一直以来,性能都是技术层面不可避开的话题,尤其在中大型复杂项目中.犹如汽车整车性能,追求极速的同时,还要保障舒适性和实用性,而在汽车制造的每个环节.零件整合情况.发动机调校等等,都会最终影响用户 ...
随机推荐
- 随机生成文章的AI(C++)
#include <iostream> #include <cstdlib> #include <ctime> #include <fstream> u ...
- Eureka使用总结
关于Eureka: 提供基于 REST的服务,在集群中主要用于服务管理.使用该框架,可以将业务组件注册到Eureka容器中,这些组件可进行集群部署,Eureka主要维护这些服务的列表并自动检查他们的状 ...
- 一文了解MySQL性能测试及调优中的死锁处理方法,你还看不明白?
一文了解MySQL性能测试及调优中的死锁处理方法,你还看不明白? 以下从死锁检测.死锁避免.死锁解决3个方面来探讨如何对MySQL死锁问题进行性能调优. 死锁检测 通过SQL语句查询锁表相关信息: ( ...
- Proxychains完成Linux命令行代理
前言 Proxychains是一个Linux和类Unix平台非常流行的命令行代理工具,它支持强制应用的TCP 连接通过代理,支持 Tor.HTTP与 Socks 代理.与 sshuttle 不同的是, ...
- 解决Mybatis 报错Invalid bound statement (not found)
解决Mybatis 报错Invalid bound statement (not found) 出现此错误的原因 1.xml文件不存在 2.xml文件和mapper没有映射上 namespace指定映 ...
- Intellij IDEA 2021.2.3 最新版免费激活教程(可激活至 2099 年,亲测有效)
申明,本教程 Intellij IDEA 最新版破解.激活码均收集与网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除.如条件允许,建议大家购买正版. 本教程更新于:2021 年 10 月 ...
- Spring Cloud Gateway 网关限流
Spring Cloud Gateway 限流 一.背景 二.实现功能 三.网关层限流 1.使用默认的redis来限流 1.引入jar包 2.编写配置文件 3.网关正常响应 4.网关限流响应 2.自定 ...
- 【Golang详解】go语言中并发安全和锁
go语言中并发安全和锁 首先可以先看看这篇文章,对锁有些了解 [锁]详解区分 互斥锁.⾃旋锁.读写锁.乐观锁.悲观锁 Mutex-互斥锁 Mutex 的实现主要借助了 CAS 指令 + 自旋 + 信号 ...
- si macro macro
获取 buf 里的 symbol cbuf = BufListCount() msg(cbuf) ibuf = 0 while (ibuf < cbuf) { hbuf = BufListIte ...
- hdu 2199 Can you solve this equation?(二分法求多项式解)
题意 给Y值,找到多项式 8*x^4 + 7*x^3 + 2*x^2 + 3*x + 6 == Y 在0到100之间的解. 思路 从0到100,多项式是单调的,故用二分法求解. 代码 double c ...