为了账号安全,请及时绑定邮箱和手机立即绑定

Java异常有多慢?

Java异常有多慢?

月关宝盒 2019-06-16 15:43:02
Java异常有多慢?问题:Java中的异常处理实际上很慢吗?传统的智慧,以及谷歌的许多结果,都认为在Java中正常的程序流不应该使用特殊的逻辑。通常有两个原因,它真的很慢-甚至比普通代码慢一个数量级(给出的原因各不相同),和这很麻烦,因为人们期望只有异常代码才能处理错误。这个问题是关于#1的。举个例子,本页将Java异常处理描述为“非常慢”,并将慢速与异常消息字符串的创建联系起来-“然后该字符串用于创建抛出的异常对象,这并不快。”文章Java中的有效异常处理他说:“这是因为异常处理的对象创建方面,从而使抛出异常的速度变慢”。另一个原因是堆栈跟踪生成减慢了它的速度。我的测试(使用Java 1.6.0_07,Java HotSpot 10.0,在32位Linux上)表明异常处理并不比常规代码慢。我尝试在执行某些代码的循环中运行一个方法。在方法的末尾,我使用一个布尔值来指示是否回归或抛出..这样,实际处理是相同的。我尝试以不同的顺序运行这些方法,并平均测试时间,认为这可能是JVM的热身。在我所有的测试中,投球至少和返回速度一样快,如果不是更快的话(高达3.1%的速度)。我完全愿意相信我的测试是错误的,但在过去的一两年里,我还没有看到在代码示例、测试比较或结果中显示Java异常处理速度慢的任何东西。导致我走上这条道路的是我需要使用的API,它将异常抛出作为正常控制逻辑的一部分。我想纠正他们的用法,但现在我可能不能。我会否因为他们的前瞻而赞扬他们呢?在报纸上实时编译中有效的Java异常处理作者认为,仅存在异常处理程序,即使没有抛出异常,也足以阻止JIT编译器正确地优化代码,从而减慢代码的速度。我还没验证过这个理论
查看完整描述

3 回答

?
手掌心

TA贡献1942条经验 获得超3个赞

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

Sun和其他人认识到,这可能不是最优的,当然,VM会随着时间的推移变得越来越快。还有另一种实现异常的方法,它可以使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 2method2 took 1003 ms, result was 2method3 took 66716 ms, result was 2

TRY块的减速太小,不能排除诸如后台进程之类的混淆因素。但是,捕获块杀死了一切,使它慢了66倍!

正如我说过的,如果将TRY/CATCH并将所有抛入相同的方法(方法3)中,结果不会那么糟糕,但是这是一个我不会依赖的特殊的JIT优化。即使在使用此优化时,抛出仍然相当缓慢。所以我不知道你在做什么,但是肯定有比尝试/捕捉/扔更好的方法。


查看完整回答
反对 回复 2019-06-16
?
犯罪嫌疑人X

TA贡献2080条经验 获得超4个赞

FYI,我扩展了Mecki所做的实验:

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

前3款和Mecki的一样(我的笔记本电脑明显慢些)。

方法4与方法3相同,只是它创建了一个new Integer(1)而不是做throw new Exception().

方法5与方法3类似,只是它创建了new Exception()不用扔了。

Method 6与Method 3类似,只不过它抛出一个预先创建的异常(一个实例变量),而不是创建一个新的异常。

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


查看完整回答
反对 回复 2019-06-16
?
繁星coding

TA贡献1797条经验 获得超4个赞

Aleksey Shipil v非常彻底的分析在其中,他在各种条件组合下对Java异常进行了基准测试:

  • 新创建的异常与预先创建的异常
  • 启用堆栈跟踪的VS禁用
  • 堆栈跟踪请求与从未请求
  • 在最高级别抓到,在每个级别重新抛出,在每个级别上被链锁/包装
  • 不同级别的Java调用堆栈深度
  • 没有内联优化和极端内联与默认设置
  • 用户定义的字段读取和未读取

他还将它们与在不同错误频率级别上检查错误代码的性能进行比较。

结论(逐字引用其职务)如下:

  1. 真正的例外是出色的表演。如果您按照设计使用它们,并且只在由常规代码处理的大量非例外情况中进行真正的例外情况交流,那么使用异常将是性能上的胜利。

  2. 例外情况的执行费用有两个主要组成部分:栈迹构造当异常被实例化并且堆栈展开在异常抛出过程中。

  3. 堆栈跟踪施工成本与堆垛深度成正比。在异常时刻实例化。这已经很糟糕了,因为地球上有谁知道这个抛出方法会被调用的堆栈深度?即使关闭堆栈跟踪生成和/或缓存异常,也只能消除这部分性能成本。

  4. 堆栈展开成本取决于我们在编译代码中更接近异常处理程序的幸运程度。仔细构造代码以避免深入的异常处理程序查找可能有助于我们变得更幸运。

  5. 如果消除这两种影响,例外的性能成本就是本地分支的性能成本。无论听起来多么美妙,这并不意味着您应该使用异常作为通常的控制流,因为在这种情况下优化编译器是任由您摆布的!您应该只在真正特殊的情况下使用它们,在这种情况下,异常频率摊销增加实际异常可能带来的不幸代价。

  6. 乐观的经验法则似乎是10^-4异常的频率是足够的。当然,这取决于异常本身的重量级、异常处理程序中所采取的确切操作等。

其结果是,当未抛出异常时,不需要支付成本,因此,当异常条件足够罕见时,异常处理要比使用if每次。这篇文章全文非常值得一读。


查看完整回答
反对 回复 2019-06-16
  • 3 回答
  • 0 关注
  • 524 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信