JVM小技巧:用throw-catch进行信号传递的加速方法

问题引入

在Java和Kotlin等JVM语言中提供了一种打破原有控制流的方式,那就是异常处理。通过throw抛出的异常会沿着调用栈回溯直到被catch。通过这种特殊的跳转,我们也可以实现一些信号的传递。

例如,一个login()函数可以通过返回不同种类的异常的方式表示程序的不同状态,同时避免执行不应当执行的代码(如服务器异常就不要执行数据库查询):如ServerNotReadyException,PasswordIncorrectException,NoSuchUserException来向调用者甚至用户直接提示错误。

更例如,在写一个基于递归的解释器(Tree-Walk Interpreter)时,一个while循环大概可以这么实现:

while(true){
    if(!evaluate(loopCondition)){
        break;
    }
    execute(loopBody);
}

如果我们想要实现breakcontinue语句,由于是递归实现的,我们不容易像汇编语言那样直接指名跳转。利用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
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录