JVM小技巧:用throw-catch进行信号传递的加速方法
问题引入
在Java和Kotlin等JVM语言中提供了一种打破原有控制流的方式,那就是异常处理。通过throw抛出的异常会沿着调用栈回溯直到被catch。通过这种特殊的跳转,我们也可以实现一些信号的传递。
例如,一个login()
函数可以通过返回不同种类的异常的方式表示程序的不同状态,同时避免执行不应当执行的代码(如服务器异常就不要执行数据库查询):如ServerNotReadyException
,PasswordIncorrectException
,NoSuchUserException
来向调用者甚至用户直接提示错误。
更例如,在写一个基于递归的解释器(Tree-Walk Interpreter)时,一个while循环大概可以这么实现:
while(true){
if(!evaluate(loopCondition)){
break;
}
execute(loopBody);
}
如果我们想要实现break
和continue
语句,由于是递归实现的,我们不容易像汇编语言那样直接指名跳转。利用Break和Continue打破原有控制流的特性,我们可以这么巧妙的实现:
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):
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()
}
输出结果如下:
4513
32
呃啊!慢了141倍!!!网上流传不假!那么为什么Exception慢呢?原因有二:
- Throwable在throw的时候需要填写stacktrace,这非常cost-heavy!——但是作为信号传递工具来说我们不关心!
- Exception每次throw就要创建Exception对象,不仅耗时还给GC上压力!——假如不需要传递message之类的,完全可以单例化!
好心的是,Java非常的宽容,Exception允许我们自定义填不填写stacktrace,通过复写fillInStackTrace方法即可:
class LightweightException() : RuntimeException() {
override fun fillInStackTrace(): Throwable {
return this // Do nothing, effectively preventing stack trace capture
}
}
也有其他方法,读者可以自行搜索~
所以我们就有了下面的优化代码:
class LightweightException() : RuntimeException() {
override fun fillInStackTrace(): Throwable {
return this // Do nothing, effectively preventing stack trace capture
}
}
val ex=LightweightException()
// If else structure
fun test4(){
val time=System.currentTimeMillis()
var count=0
for(i in 1..10000000){
if(i%2==i%3){
count++
}
}
println(System.currentTimeMillis()-time)
}
//Throw without stacktrace + singleton
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)
}
//Throw without stacktrace
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)
}
//Throw normally
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()
}
运行结果:
4513
101
38
32
可以看到,优化非常明显!完全没有负担哦!
(注意:使用单例化的Exception并不一定是一个好的实践,会带来一些其他的负面效果,在本例中可以适用而已!)
总结
不难发现,throw-catch实现本身带来的性能损失是小的,主要耗时发生在填写stacktrace中。通过一些优化,我们也可以在实践条件下放心的使用throw-catch进行信号传递!
当然,你可能会说这个benchmark做的太不严谨了!没有真实模拟现实存在的情况,也没有考虑函数嵌套等的影响!事实上也确实如此,所以欢迎各位读者自行设计benchmark,如果有其他看法,也可以在评论区留言(如果您在XGN Blog上看到了这篇文章,请移步blog.hellholestudios.top进行评论)
彩蛋
blame someone else(狗头)
@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),
// Add more fake stack frames as needed
};
super.setStackTrace(fakeStackTrace);
return this;
}
输出:
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)
版权声明:
作者:XGN
链接:https://blog.hellholestudios.top/archives/1745
来源:Hell Hole Studios Blog
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论