Java中异常对性能有何影响?

560

问题:Java中的异常处理实际上很慢吗?

传统智慧以及许多谷歌搜索结果都表明,在Java中,异常逻辑不应用于正常程序流程。通常给出两个原因:

  1. 它非常缓慢——甚至比常规代码慢一个数量级(给出的原因有所不同),

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

这个问题涉及到第一个原因。

例如,this page将Java异常处理描述为“非常缓慢”,并将其缓慢性与异常消息字符串的创建相关联——“然后使用该字符串创建抛出的异常对象。这不快。”文章Effective Exception Handling in Java表示,“原因是由于异常处理的对象创建方面,从而使抛出异常本质上变慢”。还有另外一个原因是堆栈跟踪生成会导致其变慢。

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

导致我走这条路的是我需要使用的API将异常作为正常控制逻辑的一部分抛出。我想在使用中更正它们,但现在我可能无法这样做。我是否需要称赞他们的前瞻性?

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


13
我知道你并没有询问关于第二点,但你应该认识到,使用异常来控制程序流程并不比使用GOTO更好。一些人会为GOTO进行辩护,一些人会为你所说的做法进行辩护,但如果你询问那些实施和维护过这两种方法的人,他们会告诉你这两种方法都是糟糕且难以维护的设计实践(并且可能会咒骂那个认为自己足够聪明能够决定使用它们的人)。 - Bill K
97
声称使用异常处理程序流程不比使用GOTO好不如声称使用条件和循环处理程序流程不比使用GOTO好,这是一个转移注意力的话题。解释一下,其他编程语言可以有效地使用异常处理程序流程。惯用的Python代码经常使用异常处理程序,例如。我可以并且确实维护过使用此方式的代码(不包括Java),我认为这种方式本质上没有任何问题。 - mmalone
23
在Java中,使用异常来进行正常控制流是不好的,因为这种范式选择是这样做的。请阅读Bloch EJ2——他明确指出,引用(Item 57),“异常正如其名称所示,只应用于异常情况;它们永远不应该用于普通的控制流”——并详细解释了原因。而且,他是编写Java库的人。因此,他是定义类API契约的人。/同意Bill K 的观点。 - user719662
8
如果某个框架在非异常情况下使用异常,那么它的设计存在缺陷并违反了语言中异常类的约定。仅仅因为某些人编写了糟糕的代码,并不会使其变得更好。 - user719662
11
就异常处理而言,stackoverflow.com的创始人也有错误。软件开发的黄金准则是不要让简单变得复杂和难以控制。 他写道:“确实,当你添加良好的错误检查时,本应只需三行代码的程序往往会变成48行代码,但这就是生活...” 这是对纯粹性的追求,而非简洁性。 - sf_jeff
显示剩余7条评论
18个回答

371
这取决于异常的实现方式。最简单的方法是使用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块,这将是什么样的方法,不调用任何其他方法?它只会计算一个数字吗?那么你为什么需要异常呢?有更加优雅的方式来调节程序流程。除了简单的数学运算外,几乎任何其他事情都必须调用外部方法,这已经破坏了本地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 优化,我不会依赖它。即使使用此优化,throw 仍然相当缓慢。所以我不知道您想要做什么,但绝对有比使用 try/catch/throw 更好的方法。


7
好的,我会尽力进行翻译。以下是要翻译的内容:Great answer but I'd just like to add that as far as I know, System.nanoTime() should be used for measuring performance, not System.currentTimeMillis().很好的回答,但我想补充一点,据我所知,应该使用System.nanoTime()来测量性能,而不是System.currentTimeMillis()。 - Simon Forsberg
10
nanoTime()方法需要Java 1.5以上版本支持。我编写上方代码时只有Java 1.4版本可用,因此不能使用nanoTime()。但实际上这并不影响太大。两者之间唯一的区别在于时间单位,一个是纳秒,一个是毫秒,并且nanoTime()不受时钟操作的影响(除非你或系统进程在测试代码运行的确切时刻修改了系统时钟)。总的来说,你是正确的,nanoTime()当然是更好的选择。 - Mecki
3
需要翻译的内容: It really should be noted that your test is an extreme case. You show a very small performance hit for code with a try block, but no throw. Your throw test is throwing exceptions 50% of the time it goes through the try. That's clearly a situation where the failure is not exceptional. Cutting that down to only 10% massively cuts the performance hit. The problem with this kind of test is that it encourages people to stop using exceptions altogether. Using exceptions, for exceptional case handling, performs vastly better than what your test shows.你的测试案例是一个极端情况,需要注意。在没有 throw 的代码中,使用 try 块对性能影响很小。但是,当 try 块中的代码 50% 的时间 抛出异常时,这个 throw 测试就不再是异常情况,将其减少到仅有 10% 就能显著降低性能开销。这种测试方式的问题在于它鼓励人们停止使用异常处理机制。实际上,在处理异常情况时,使用异常处理机制的性能比你的测试结果要好得多。 - Nate
6
@Glide 一个抛出异常不像一个干净的return语句。它会在方法体的某个位置离开,甚至可能是在操作的中途(到目前为止仅完成了50%的操作),而且catch块可能位于20个栈帧之上(一个方法具有一个try块,调用方法1,其中包含调用方法2,方法2又调用方法3,以此类推直至方法20,在操作的中途抛出异常)。必须向上展开20个栈帧,所有未完成的操作必须被撤销(操作不能半途而废),CPU寄存器需要处于清洁状态。这一切都需要花费时间。 - Mecki
2
在jdk 1.8.0_181上,我得到了这些结果,表明性能有所提高: method1花费928毫秒,结果为2 | method2花费1098毫秒,结果为2 | method3花费26366毫秒,结果为2 - Emanuel George Hategan
显示剩余15条评论

288

顺便提一下,我扩展了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

前三个方法与Mecki的相同(我的笔记本电脑显然比较慢)。

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

method5类似于method3,只是它创建new Exception()时不会抛出异常。

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

在Java中,抛出异常的开销主要是花费在收集堆栈跟踪信息上,这发生在异常对象被创建时。抛出异常的实际成本虽然很高,但比创建异常的成本要少得多。


59
您的答案解决了核心问题 - 解开和跟踪堆栈所需的时间,其次是抛出错误。我会选择此答案作为最终答案。 - Engineer
11
不错。约70%的时间是创建异常,30%的时间是抛出异常。很好的信息。 - chaqke
1
@Basil - 你应该能够从上面的数字中找出答案。 - Hot Licks
3
@HotLicks,这正是说明在帖子中使用了哪个版本的Java非常重要的原因。 - Thorbjørn Ravn Andersen
3
我们可以注意到,在标准代码中,创建和抛出异常很少发生(我指的是在运行时),如果不是这种情况,那么运行时条件非常糟糕,或者设计本身就是问题所在;在这两种情况下,性能都不是一个问题... - Jean-Baptiste Yunès
显示剩余4条评论

90

Aleksey Shipilëv进行了非常全面的分析,在不同条件下对Java异常进行基准测试:

  • 新创建的异常与预先创建的异常
  • 启用堆栈跟踪与禁用堆栈跟踪
  • 请求堆栈跟踪与从未请求堆栈跟踪
  • 在顶层捕获与在每个级别重新抛出与在每个级别链接/封装
  • 各种Java调用堆栈深度
  • 无内联优化与极端内联与默认设置
  • 读取用户定义字段与不读取

他还将它们与检查各种错误频率的错误代码的性能进行了比较。

结论(摘自他的帖子)是:

  1. 真正的异常情况表现出色。 如果按照设计使用它们,并且只在处理常规代码中的非异常情况时传递真正的异常情况,那么使用异常可以提高性能。

  2. 异常的性能成本有两个主要组成部分:在实例化 Exception 时构建堆栈跟踪在抛出 Exception 期间进行堆栈展开

  3. 在异常实例化时,堆栈跟踪构建成本与堆栈深度成正比。 这已经很糟糕了,因为谁知道在哪个堆栈深度调用此抛出方法?即使您关闭堆栈跟踪生成和/或缓存异常,也只能摆脱性能成本的这一部分。

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

  5. 如果消除这两种影响,则异常的性能成本就是局部分支的成本。 不管听起来多么美好,这并不意味着您应该将异常用作通常的控制流程,因为在这种情况下,您要看编译器的优化程度! 您应该只在真正的异常情况下使用它们,在这种情况下,异常频率摊销了引发实际异常的可能不幸成本。

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

当异常未被抛出时,你不需要付出代价,因此当异常情况足够罕见时,使用异常处理比每次使用if语句更快。完整的文章非常值得一读。

44

很不幸,我的答案太长无法在这里发布。因此,请容许我在此进行总结,并引用http://www.fuwjax.com/how-slow-are-java-exceptions/来查看详细信息。

实际上,这里的真正问题不是“相对于'从不出错'的代码,'以异常形式报告失败'有多慢?” 正如接受的回答可能会让你相信的那样。而是,“'以异常形式报告失败'与其他方式报告失败相比,有多慢?” 通常,报告失败的两种其他方式是使用哨兵值或结果包装器。

哨兵值是为了在成功时返回一个类,在失败时返回另一个类。您可以将其视为返回异常而不是抛出异常。这需要具有成功对象的共享父类,然后执行“instanceof”检查和一些强制类型转换以获取成功或失败信息。

事实证明,即使存在类型安全的风险,哨兵值比异常更快,但仅快大约2倍。现在,这似乎是很多,但是这2倍只涵盖了实现差异的成本。实际上,这个因素要小得多,因为我们的可能会失败的方法比此页面其他地方的一些算术运算符更有趣。

另一方面,结果包装器完全不会牺牲类型安全性。它们将成功和失败信息封装在一个类中。因此,它们提供了“isSuccess()”和获取成功和失败对象的getter,而不是“instanceof”。然而,结果对象大约比使用异常慢2倍。事实证明,每次创建新的包装器对象比有时抛出异常更加昂贵。

除此之外,异常是语言提供的指示方法可能会失败的方式。仅从API中无法告知哪些方法预计总是(或大多数情况下)有效,哪些方法预计要报告失败。

异常比哨兵更安全,比结果对象更快,且不会像两者之一那样令人惊讶。我并不是建议 try/catch 替代 if/else,但是即使在业务逻辑中,异常也是报告失败的正确方式。

话虽如此,我想指出两种最常见的严重影响性能的方式是创建不必要的对象和嵌套循环。如果您可以选择是否创建异常,请不要创建异常。如果您可以选择有时创建异常还是始终创建另一个对象,请创建异常。


5
我决定测试三种实现的长期性能,与检测故障但不报告的控制实现进行比较。该过程的故障率约为4%。一个测试迭代会对其中一种策略执行10000次该过程。每种策略被测试1000次,并使用最后900次来生成统计数据。以下是各自的平均纳秒时间: 控制实现 338 异常 429 结果 348 哨兵 345 - Fuwjax
3
为了好玩,我在异常测试中禁用了 fillInStackTrace。现在的时间如下:控制组 347,异常组 351,结果组 364,哨兵组 355。 - Fuwjax
1
Fuwjax,除非我漏掉了什么(我承认我只看了你的SO帖子,没有看你的博客帖子),否则你上面的两条评论似乎与你的帖子相矛盾。我假设在你的基准测试中较低的数字更好,对吗?如果是这样,在启用fillInStackTrace(这是默认和通常的行为)的情况下生成异常会导致比你描述的其他两种技术更慢的性能。我有什么遗漏的地方,还是你实际上是在反驳你的帖子? - Felix GV
@Fuwjax,所以根据你的说法,抛出异常不会创建对象?我不确定我理解这个推理。无论是抛出异常还是返回结果对象,都会创建对象。从这个意义上说,结果对象并不比抛出异常慢。 - mxk
@Matthias 抛出异常显然会创建一个对象。但是感谢您指出我没有表达清楚。上面的“Result Object”一词最好理解为“Result Wrapper”。因此,除了任何失败响应对象之外,您还必须创建一个结果包装器。我所指的是额外的包装器实例化。在实践中的一个很好的例子是新的Java 8 Optional对象。 - Fuwjax
显示剩余3条评论

23
我已经扩展了@Mecki@incarnate提供的答案,但是没有为Java填充堆栈跟踪。
对于Java 7+,我们可以使用Throwable(String message,Throwable cause,boolean enableSuppression,boolean writableStackTrace)。但对于Java6,请参见我在这个问题上的回答
// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    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 NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    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 NoStackTraceRuntimeException();
    }
}

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++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


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

使用Java 1.6.0_45,在Core i7,8GB RAM上输出:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

因此,相比于抛出异常的方法,返回值的方法更快。在我看来,我们无法仅使用返回类型设计一个清晰的API,用于成功和错误流程。没有堆栈跟踪的抛出异常的方法比普通异常快4-5倍。
编辑:NoStackTraceThrowable.java 感谢@Greg
public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}

有趣,谢谢。这是缺失的类声明:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } } - Greg
在开头你写了 With Java 7+,我们可以使用,但后来你又写了 Output with Java 1.6.0_45,那么这是Java 6还是7的结果? - WBAR
1
从Java 7开始,我们只需要使用具有boolean writableStackTrace参数的Throwable构造函数。但是在Java 6及以下版本中不存在该参数。这就是为什么我为Java 6及以下版本提供了自定义实现。因此,上面的代码适用于Java 6及以下版本。请仔细阅读第二段第一行。 - manikanta
@manikanta "在我看来,我们不能仅使用返回类型来设计一个清晰的API,以处理成功和错误流程。" -- 如果我们使用Optionals/Results/Maybe,就可以做到,正如许多语言所做的那样。 - Hejazzman
@Hejazzman 我同意。但是 Optional 或类似的东西来得有点晚了。在那之前,我们也使用带有成功/错误标志的包装对象。但这似乎有点不自然和不专业。 - manikanta
很抱歉,这个基准测试存在缺陷,至少在最近的Java版本中是如此。在主方法中添加一个循环以多次运行测试将显示所有5种方法在第一次循环后花费大约相同的时间。 - john16384

9
不久前,我写了一个类来测试使用两种方法将字符串转换为整数的相对性能:(1) 调用 Integer.parseInt() 并捕获异常,或者 (2) 使用正则表达式匹配字符串,并仅在匹配成功时调用 parseInt()。我尽可能有效地使用了正则表达式(即,在进入循环之前创建 Pattern 和 Matcher 对象),并且没有打印或保存异常的堆栈跟踪。
对于一万个字符串的列表,如果它们都是有效的数字,则 parseInt() 方法比正则表达式方法快四倍。但是如果只有80%的字符串是有效的,则正则表达式比 parseInt() 快两倍。如果有20% 是有效的,也就是说异常被抛出并被捕获了80% 的时间,那么正则表达式比 parseInt() 快约二十倍。
考虑到正则表达式方法会处理有效字符串两次:一次是匹配,一次是 parseInt(),我对结果感到惊讶。但是,抛出和捕获异常弥补了这一点。这种情况在现实世界中不太可能经常发生,但如果确实发生了,您绝对不应该使用捕获异常的技术。但是,如果您只验证用户输入或类似情况,请务必使用 parseInt() 方法。

你用的是哪个JVM?使用sun-jdk 6还是很慢吗? - Benedikt Waldvogel
我挖出来并在提交答案之前在JDK 1.6u10下重新运行了它,那就是我发布的结果。 - Alan Moore
这非常非常有用!谢谢。对于我的常规用例,我需要解析用户输入(使用类似 Integer.ParseInt() 的东西),并且我预计大多数情况下用户输入都是正确的,因此对于我的用例来说,似乎接受偶尔的异常错误是可行的方式。 - markvgti

8
我认为第一篇文章提到遍历调用堆栈并创建堆栈跟踪是昂贵的部分,而第二篇文章虽然没有明说,但我认为这是对象创建中最昂贵的部分。约翰·罗斯有一篇文章描述了不同的技术来加速异常处理。(预分配和重复使用异常,无堆栈跟踪的异常等)
但是 - 我认为这只应该被视为必要的恶,最后的手段。约翰之所以这样做,是为了模仿其他语言中尚未(或尚未)在JVM中提供的功能。您不应该养成使用异常进行控制流程的习惯。特别是出于性能原因!正如您自己在#2中提到的那样,这种方式会导致掩盖代码中的严重错误,并且对于新程序员来说维护会更加困难。
Java中的微基准测试出奇地难以正确实现(我被告知),特别是当你涉及JIT领域时,所以我真的怀疑在实际生活中使用异常是否比“return”更快。例如,我怀疑你的测试中有2到5个堆栈帧?现在想象一下,你的代码将由JBoss部署的JSF组件调用。现在你可能会有一个几页长的堆栈跟踪。
也许你可以发布你的测试代码?

1
链接已失效。 - Glenn

8
我不知道这些话题是否相关,但我曾经想要实现一种技巧,依赖于当前线程的堆栈跟踪。我想发现触发实例化内部类中的方法名称(是的,这个想法很疯狂,我完全放弃了它)。所以我发现调用Thread.currentThread().getStackTrace()非常缓慢(由于它在内部使用的本地dumpThreads方法)。
因此,Java Throwable相应地具有一个本地方法fillInStackTrace。我认为前面描述的致命catch块以某种方式触发了该方法的执行。
但让我告诉你另一个故事...
在Scala中,一些函数特性使用ControlThrowable在JVM上编译,它扩展了Throwable并以以下方式覆盖了其fillInStackTrace
override def fillInStackTrace(): Throwable = this

因此,我改编了上面的测试(循环次数减少了十次,我的机器有点慢 :):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

因此,结果如下:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

您看,method3method4之间唯一的区别就是它们抛出不同类型的异常。是的,method4仍然比method1method2慢,但差距更加可接受。


5

Java 和 C# 中的异常性能仍有很大提升空间。

作为程序员,这迫使我们遵循“异常应该尽可能少地发生”的规则,仅出于实际性能原因。

然而,作为计算机科学家,我们应该反对这种问题。编写函数的人通常不知道它将被调用多少次,或者成功或失败的可能性更大。只有调用者才有这些信息。试图避免异常会导致不清晰的 API 习语,在某些情况下,我们只有干净但缓慢的异常版本,在其他情况下,我们有快速但笨重的返回值错误,在其他情况下,我们最终两者都有。库实现者可能需要编写和维护两个版本的 API,而调用者必须在每种情况下决定使用哪个版本。

这有点混乱。如果异常的性能更好,我们就可以避免这些笨拙的习语,并像它们的本意一样使用异常...作为结构化错误返回工具。

我真的希望看到使用更接近返回值的技术来实现异常机制,这样我们就可以获得更接近返回值的性能...因为这是我们在性能敏感的代码中所采用的。

这是一个比较异常性能和错误返回值性能的代码示例。

public class TestIt {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

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

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

以下是结果:

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

检查和传播返回值会增加一些成本,相较于基线-空调用,这个成本与调用深度成正比。在调用链深度为8的情况下,错误返回值检查版本比未检查返回值的基线版本慢约27%。

相比之下,异常性能不是调用深度的函数,而是异常频率的函数。然而,随着异常频率的增加,其退化情况更为剧烈。仅在25%的错误频率下,代码运行速度变慢了24倍。在100%的错误频率下,异常版本几乎变慢了100倍。

这让我想到,也许我们在异常实现方面做出了错误的权衡。异常可以更快地处理,通过避免昂贵的堆栈跟踪或直接将它们转换为编译器支持的返回值检查。在它们这样做之前,当我们想要使代码运行得更快时,我们只能避免使用异常。


5
我已经使用JVM 1.5进行了一些性能测试,并且使用异常至少要慢2倍。平均而言:在一个非常小的方法上执行时间增加了三倍以上(3倍),必须捕获异常的小循环自我时间增加了2倍。
我在生产代码和微基准测试中看到了类似的数据。
不应该经常调用任何东西来抛出异常。每秒抛出数千个异常会导致巨大的瓶颈。
例如,使用“Integer.ParseInt(...)”在非常大的文本文件中查找所有错误值-非常糟糕的想法。(我在生产代码中看到过这个效用方法导致性能下降)
在用户GUI表单上报告不良值使用异常从性能角度来看可能就没那么糟糕。
无论它是否是一个好的设计实践,我会遵循以下规则:如果错误是正常/预期的,则使用返回值。如果是异常情况,则使用异常。例如:读取用户输入,不良值是正常的-使用错误代码。传递值给内部实用程序函数时,调用代码应过滤不良值-使用异常。

让我建议一些好的做法:如果你需要一个表单中的数字,而不是使用Integer.valueOf(String),你应该考虑使用正则表达式匹配器。你可以预编译和重用模式,这样制作匹配器就很便宜了。然而,在GUI表单上,拥有isValid/validate/checkField或其他类似的东西可能更清晰。此外,随着Java 8的Optional monads,考虑使用它们。(答案已经有9年了,但仍然适用!:p) - Haakon Løtveit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接