小编典典

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

all

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

传统观点以及许多 Google 结果都表明,异常逻辑不应该用于 Java 中的正常程序流。通常给出两个原因,

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

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

这个问题是关于#1。

作为示例,此页面
Java 异常处理描述为“非常慢”,并将慢速与异常消息字符串的创建联系起来——“此字符串随后用于创建抛出的异常对象。这并不快。” 《Java
中的有效异常处理
》一文说,“原因在于异常处理的对象创建方面,这使得抛出异常本质上很慢”。另一个原因是堆栈跟踪的生成会减慢它的速度。

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

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

在论文Efficient Java exception handling in just-in-time
compilation
中,作者建议仅存在异常处理程序,即使没有抛出异常,也足以阻止
JIT 编译器正确优化代码,从而减慢它的速度. 我还没有测试过这个理论。


阅读 117

收藏
2022-03-10

共1个答案

小编典典

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

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

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

在 try 块中执行的操作也会产生很大的不同。如果您打开一个 try 块并且从不从该 try 块中调用任何方法,则该 try 块将非常快,因为 JIT
实际上可以将 throw 视为简单的 goto。它既不需要保存堆栈状态,也不需要在抛出异常时展开堆栈(它只需要跳转到 catch
处理程序)。但是,这不是您通常会做的事情。通常你打开一个 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 块的减速太小,无法排除诸如后台进程之类的混杂因素。但是 catch 块杀死了一切,让它慢了 66 倍!

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

2022-03-10