问题引入 在Java和Kotlin等JVM语言中提供了一种打破原有控制流的方式,那就是异常处理。通过throw抛出的异常会沿着调用栈回溯直到被catch。通过这种特殊的跳转,我们也可以实现一些信号的传递。
例如,一个login()
函数可以通过返回不同种类的异常的方式表示程序的不同状态,同时避免执行不应当执行的代码(如服务器异常就不要执行数据库查询):如ServerNotReadyException
,PasswordIncorrectException
,NoSuchUserException
来向调用者甚至用户直接提示错误。
更例如,在写一个基于递归的解释器(Tree-Walk Interpreter)时,一个while循环大概可以这么实现:
1 2 3 4 5 6 while (true ){ if (!evaluate(loopCondition)){ break ; } execute(loopBody); }
如果我们想要实现break
和continue
语句,由于是递归实现的,我们不容易像汇编语言那样直接指名跳转。利用Break和Continue打破原有控制流的特性,我们可以这么巧妙的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void executeBreak () { throw new BreakException(); } void executeContinue () { throw new ContinueException(); } void executeOthers () { } void executeWhile () { while (true ){ if (!evaluate(loopCondition)){ break ; } try { execute(loopBody); }catch (BreakException e){ break ; }catch (ContinueException e){ continue ; } } }
显然,上述程序都可以写成返回enum+message的形式,然后手动return实现控制流回退。这样更类似于C和Rust的实现理念。
然而,许多时候,我们被告知不应当使用异常进行信号传递,主要理由是:performance。网上流传的说法是throw比正常的if判断会慢几百到几千倍,如果一个接口需要频繁被调用,那么使用throw进行信号传递是非常低效的行为。
这个说法是真的吗?如果确实如此,我们真的就应该断念throw-catch吗?本文将探讨如何在该情境下实现throw-catch信号传递的加速。
分析 分析下列程序(Kotlin):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 fun test2 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ if (i%2 ==i%3 ){ count++ } } println(System.currentTimeMillis()-time) } fun test1 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ try { if (i%2 ==i%3 ) throw RuntimeException() }catch (_:Exception){ count++ } } println(System.currentTimeMillis()-time) } fun main () { test1() test2() }
输出结果如下:
呃啊!慢了141倍!!!网上流传不假!那么为什么Exception慢呢?原因有二:
Throwable在throw的时候需要填写stacktrace,这非常cost-heavy!——但是作为信号传递工具来说我们不关心!
Exception每次throw就要创建Exception对象,不仅耗时还给GC上压力!——假如不需要传递message之类的,完全可以单例化!
好心的是,Java非常的宽容,Exception允许我们自定义填不填写stacktrace,通过复写fillInStackTrace方法即可:
1 2 3 4 5 class LightweightException () : RuntimeException() { override fun fillInStackTrace () : Throwable { return this } }
也有其他方法,读者可以自行搜索~
所以我们就有了下面的优化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 class LightweightException () : RuntimeException() { override fun fillInStackTrace () : Throwable { return this } } val ex=LightweightException()fun test4 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ if (i%2 ==i%3 ){ count++ } } println(System.currentTimeMillis()-time) } fun test3 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ try { if (i%2 ==i%3 ) throw ex }catch (_:Exception){ count++ } } println(System.currentTimeMillis()-time) } fun test2 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ try { if (i%2 ==i%3 ) throw LightweightException() }catch (_:Exception){ count++ } } println(System.currentTimeMillis()-time) } fun test1 () { val time=System.currentTimeMillis() var count=0 for (i in 1. .10000000 ){ try { if (i%2 ==i%3 ) throw RuntimeException() }catch (_:Exception){ count++ } } println(System.currentTimeMillis()-time) } fun main () { test1() test2() test3() test4() }
运行结果:
可以看到,优化非常明显!完全没有负担哦!
(注意:使用单例化的Exception并不一定是一个好的实践,会带来一些其他的负面效果,在本例中可以适用而已!)
总结 不难发现,throw-catch实现本身带来的性能损失是小的,主要耗时发生在填写stacktrace中。通过一些优化,我们也可以在实践条件下放心的使用throw-catch进行信号传递!
当然,你可能会说这个benchmark做的太不严谨了!没有真实模拟现实存在的情况,也没有考虑函数嵌套等的影响!事实上也确实如此,所以欢迎各位读者自行设计benchmark,如果有其他看法,也可以在评论区留言(如果您在XGN Blog上看到了这篇文章,请移步blog.hellholestudios.top进行评论)
彩蛋 blame someone else(狗头)
1 2 3 4 5 6 7 8 9 10 11 @Override public Throwable fillInStackTrace () { StackTraceElement[] fakeStackTrace = new StackTraceElement[] { new StackTraceElement("top.hhs.zzzyt.absolutely.correct.code" , "someMethod" , "SomeClass.java" , 114514 ), new StackTraceElement("definitely.not.my.code.lol" , "processData" , "Module.java" , 1919810 ), new StackTraceElement("blame.someone.else" , "runTask" , "OldCode.java" , 0 ), }; super .setStackTrace(fakeStackTrace); return this ; }
输出:
1 2 3 4 BlameException: Something went wrong... or did it? at top.hhs.zzzyt.absolutely.correct.code.someMethod(SomeClass.java:114514) at definitely.not.my.code.lol.processData(Module.java:1919810) at blame.someone.else.runTask(OldCode.java:0)