异常对 Java 中的性能有什么影响?

2022-08-31 04:19:44

问:Java 中的异常处理真的很慢吗?

传统观点以及许多Google结果都认为,在Java中,不应该使用特殊的逻辑来执行正常的程序流。通常给出两个原因,

  1. 它真的很慢 - 甚至比常规代码慢一个数量级(给出的原因各不相同),

  1. 这很混乱,因为人们期望在异常代码中只处理错误。

这个问题是关于#1的。

例如,本页将 Java 异常处理描述为“非常慢”,并将慢速与异常消息字符串的创建相关联 - “然后使用此字符串来创建引发的异常对象。这并不快。Java中的有效异常处理一文说:“其原因是由于异常处理的对象创建方面,因此使抛出异常本身就很慢”。另一个原因是堆栈跟踪生成会减慢它的速度。

我的测试(在32位Linux上使用Java 1.6.0_07,Java HotSpot 10.0)表明异常处理并不比常规代码慢。我尝试在执行一些代码的循环中运行一个方法。在方法的末尾,我使用布尔值来指示是返回还是抛出。这样,实际处理是相同的。我尝试以不同的顺序运行这些方法,并平均我的测试时间,认为这可能是JVM预热。在我所有的测试中,投掷速度至少与返回速度一样快,如果不是更快(最多快3.1%)。我对我的测试是错误的可能性持完全开放的态度,但是在过去的一两年中,我还没有看到任何代码示例,测试比较或结果显示Java中的异常处理实际上很慢的东西。

引导我走上这条路的是我需要使用的API,它将抛出异常作为正常控制逻辑的一部分。我想纠正它们的用法,但现在我可能无法做到。相反,我必须赞扬他们的前瞻性思维吗?

在《实时编译中的高效 Java 异常处理》一文中,作者建议,即使没有抛出异常,异常处理程序的存在也足以阻止 JIT 编译器正确优化代码,从而减慢其速度。我还没有测试这个理论。


答案 1

这取决于异常的实现方式。最简单的方法是使用 setjmp 和 longjmp。这意味着CPU的所有寄存器都写入堆栈(这已经需要一些时间),并且可能需要创建一些其他数据...所有这些都已经发生在try语句中。throw 语句需要展开堆栈并还原所有寄存器的值(以及 VM 中可能的其他值)。因此,尝试和抛出同样慢,而且非常慢,但是如果没有抛出异常,在大多数情况下退出try块不需要任何时间(因为所有内容都放在堆栈上,如果方法存在,则会自动清理)。

Sun和其他人认识到,这可能是次优的,当然,随着时间的推移,VM变得越来越快。还有另一种实现异常的方法,这使得trye本身变得闪电般(实际上,一般来说,try不会发生任何事情 - 当VM加载类时,需要发生的所有事情都已经完成),并且它使抛出不那么慢。我不知道哪个JVM使用这种新的,更好的技术...

...但是你是用Java编写的,所以你的代码以后只能在一个特定系统上的一个JVM上运行?因为如果它可能在任何其他平台或任何其他JVM版本(可能是任何其他供应商)上运行,谁说他们也使用快速实现?快速的比慢的更复杂,并且在所有系统上都不容易实现。您想保持便携吗?那么不要依赖异常速度快。

这也对你在尝试块中执行的操作有很大的不同。如果您打开一个 try 块,并且从不从此 try 块中调用任何方法,则 try 块将非常快,因为 JIT 实际上可以将抛出视为简单的 goto。它既不需要保存堆栈状态,也不需要在引发异常时展开堆栈(它只需要跳转到 catch 处理程序)。但是,这不是您通常执行的操作。通常,您打开一个 try 块,然后调用一个可能引发异常的方法,对吗?即使您只是在方法中使用 try 块,这将是什么类型的方法,不调用任何其他方法?它会只计算一个数字吗?那么,您需要什么例外呢?有更优雅的方法来调节程序流。除了简单的数学之外,几乎其他任何东西,你都必须调用一个外部方法,这已经破坏了本地try块的优势。

请参阅以下测试代码:

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

结果:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

try 块的速度太小,无法排除背景进程等混杂因素。但是捕获块杀死了一切,并使其速度变慢了66倍!

正如我所说,如果你把try/catch和抛出都放在同一个方法(method3)中,结果不会那么糟糕,但这是一个特殊的JIT优化,我不会依赖。即使使用此优化,投掷仍然很慢。所以我不知道你在这里想做什么,但肯定有比使用尝试/接球/投掷更好的方法。


答案 2

仅供参考,我扩展了Mecki所做的实验:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

前3个与Mecki的相同(我的笔记本电脑显然更慢)。

method4 与 method3 相同,只是它创建了一个 而不是执行 。new Integer(1)throw new Exception()

method5 与 method3 类似,只是它创建了不抛出它。new Exception()

method6 与 method3 类似,只是它引发一个预先创建的异常(一个实例变量),而不是创建一个新的异常。

在 Java 中,引发异常的大部分费用是收集堆栈跟踪所花费的时间,这在创建异常对象时发生。引发异常的实际成本虽然很大,但远低于创建异常的成本。


推荐