如果从未抛出异常,使用try-catch块是否昂贵?

219

我们知道捕获异常很昂贵。但是,即使没有抛出异常,使用Java中的try-catch块是否也很昂贵?

我在Stack Overflow上找到了一个问题/答案为什么 try 块很昂贵? ,但它是针对.NET的。


32
这个问题其实没有意义。Try..catch有一个非常具体的目的。如果需要它,那就需要使用它。无论如何,如果没有catch,try还有什么意义呢? - JohnFx
54
try { /* do stuff */ } finally { /* make sure to release resources */ } 是合法且有用的代码结构。 - A4L
4
这个成本必须与收益相比较,它并不是单独存在的。无论如何,昂贵是相对的,除非你知道你做不到,否则使用最明显的方法比因为它可能会在程序执行过程中节省一毫秒或两毫秒而什么都不做是有意义的。 - Joel
4
希望这不是一个“让我们重新发明错误代码”的情况引导... - mikołak
6
使用Java7,您甚至可以使用“try-with-resources”语句块来取代“finally”块。 - user330315
显示剩余15条评论
7个回答

233

try 几乎没有任何开销。它不是在运行时设置try,而是在编译时结构化代码的元数据,以使得当抛出异常时,它现在执行相对昂贵的操作:遍历堆栈并查看是否存在任何能够捕获此异常的 try 块。从外行人的角度来看,try 几乎就像是免费的。实际上,你需要付出代价的是抛出异常,但除非你抛出了数百或数千个异常,否则你仍然不会注意到这个开销。


try 有一些轻微的成本与之相关联。Java不能对 try 块中的代码进行某些优化。例如,Java通常会重新排列方法中的指令以使其运行更快 - 但Java也需要保证如果抛出异常,则该方法的执行必须被观察为执行源代码中按顺序编写的语句达到某个行。

因为在 try 块中可能会抛出异常(在块中的任何一行!有些异常是异步抛出的,例如通过调用线程的 stop(已弃用), 除此之外,OutOfMemoryError 几乎可以发生在任何地方),但它仍然可以被捕获并且在同一个方法中继续执行,所以更难推理可行的优化方法,因此它们不太可能发生(某人必须编写编译器来执行这些操作,推理和保证正确性等。对于要“异常”的事情来说,这将是一个巨大的痛苦)。但是,在实践中,您不会注意到这些事情。


2
一些异常是异步抛出的,它们不是异步的,而是在安全点抛出。而且这部分“try有一些与之相关的小成本。Java无法对try块中的代码进行一些优化,否则它会执行”确实需要一个严肃的参考。在某些时候,代码很可能会在try/catch块内。尽管try/catch块更难被内联和构建适当的结果格子,但重新排列的部分是含糊不清的。 - bestsss
3
没有catchtry...finally块是否也会阻止一些优化? - dajood
6
“实际上让你付出代价的是抛出异常”,从技术角度来说,抛出异常并不昂贵;而实例化“Exception”对象是占用时间最多的。 - Austin

83

让我们来测量一下,好吗?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

在我的电脑上,这将打印出类似以下内容:
try     0.598 ns
no try  0.601 ns

至少在这个简单的例子中,try语句对性能没有可测量的影响。可以随意测量更复杂的例子。
一般来说,我建议在您的代码中出现实际性能问题之前,不要担心语言结构的性能成本。或者正如Donald Knuth所说:“过早优化是万恶之源”。

8
尝试/不尝试在大多数JVM上很可能是相同的,但微基准测试非常有缺陷。 - bestsss
2
很多层面:你的意思是结果在不到1纳秒内计算出来?编译后的代码将同时删除try/catch和循环(1到n的数字求和是一种简单的算术级数求和)。即使代码包含try/finally,编译器也可以证明其中没有任何内容可以被抛出。抽象代码只有2个调用站点,它将被克隆并内联。还有更多情况,请查阅一些关于微基准测试的文章,并且如果您决定编写微基准测试,请始终检查生成的汇编代码。 - bestsss
3
所报告的时间是每次循环迭代的时间。仅当测量总经过时间> 0.1秒(或20亿次迭代,但这里并非如此)时,才会使用一项测量。我发现你声称整个循环已被删除的说法难以置信 - 因为如果循环被删除了,那么执行需要0.1秒的操作是什么? - meriton
根据-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly,生成的本地代码中确实包含循环和其中的加法。抽象方法没有被内联,因为它们的调用者没有被即时编译(可能是因为调用次数不够)。 - meriton
1
如何在Java中编写正确的微基准测试:https://dev59.com/hHRB5IYBdhLWcg3wz6UK - Vadzim
@Vadzim:我知道那个链接……你想让我注意其中哪些最佳实践?据我所知,上述基准测试工具已经满足了所有的最佳实践……除了使用库,因为在堆栈溢出的答案中链接库会使我的测量更难以验证。 - meriton

53

try/catch 可能会对性能产生一定的影响,因为它阻止了JVM进行某些优化。《Effective Java》一书中,Joshua Bloch指出:

• 将代码放在try-catch块中会抑制现代JVM实现可能执行的某些优化。


29
“它阻止了JVM进行某些优化”...你能详细说明一下吗? - The Kraken
5
通常来说,位于 try 代码块内的代码不能与 try 块外的代码进行重新排序,这是其中一个例子。请注意,我尽力保持原文意思不变,同时让翻译更加易懂。 - Patashu
3
请注意,问题是关于“它是否昂贵”,而不是关于“它是否对性能有影响”。 - mikołak
4
我从《Effective Java》里加入了一段摘录,那可是Java的圣经啊;除非有相关的参考,否则这段摘录并没有提供任何信息。在Java中,实际上几乎所有的代码都在某个级别上包含了try/finally块。 - bestsss

34

是的,正如其他人所说,try块会抑制围绕它的{}字符之间的某些优化。特别地,优化器必须假设异常可能发生在块内的任何位置,因此无法保证语句得到执行。

例如:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

如果没有try,计算用于赋值给x的值可能会被保存为“公共子表达式”,并重复用于赋值给y。但由于有了try,无法保证第一个表达式已经被计算过,因此必须重新计算该表达式。在“直线”代码中,这通常不是什么大问题,但在循环中可能会产生明显的影响。

然而需要指出的是,这仅适用于经过JIT编译的代码。javac只进行了微不足道的优化,对于字节码解释器进入/退出try块没有任何成本。(没有生成字节码来标记块边界。)

还有对于bestsss:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

输出:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

javap 输出:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

不要使用 "GOTO"。


没有生成任何字节码来标记块边界,这并不一定需要使用GOTO离开块,否则它将掉入catch/finally框架中。 - bestsss
即使生成了GOTO(这并不是必然的),其成本也微不足道,而且它远非块边界的“标记”——GOTO可以为许多结构生成。 - Hot Licks
我从未提到成本,但是“没有生成字节码”是错误的陈述。这就是全部。实际上,在字节码中没有块,帧不等于块。 - bestsss
如果try块直接进入finally块,或者其他一些情况下将不会出现GOTO跳转。重要的是,没有类似于“进入try”/“退出try”的字节码指令。 - Hot Licks
如果try块直接进入finally块,则不会有GOTO - 错误!字节码中没有finally,而是try/catch(Throwable any){...; throw any;}。它确实有带有框架和必须定义(非空)的Throwable的catch语句等等。为什么要争论这个话题,你可以至少检查一些字节码吗?当前实现finally的指南是复制块并避免goto部分(以前的实现),但是字节码必须根据有多少出口点进行复制。 - bestsss
我并没有说字节码中有finally。不过在Java中是有的。 - Hot Licks

9

又一个微基准测试(源代码)。

我创建了一个测试,用于测量基于异常百分比的try-catch和无try-catch代码版本。10%的百分比意味着测试用例中有10%的除零情况。在一种情况下,它通过try-catch块处理,在另一种情况下,则由条件运算符处理。以下是我的结果表:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
百分比 | 结果(尝试/如果,ns)
    0%     |      88/90   
    1%     |      89/87    
    10%    |      86/97    
    90%    |      85/83   

这表明在这些情况下没有显着的差异。


8
为了理解为什么无法进行优化,了解底层机制是很有用的。我找到的最简洁的例子是用C宏实现的,网址是:http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

编译器常常难以确定跳跃是否可以局部化到X、Y和Z,因此它们会跳过无法保证安全性的优化,但实现本身相当轻巧。

4
你找到的这些C宏用于try/catch,与Java或C#实现不等效,后者在运行时不会生成任何指令。 - Patashu
Java的实现过于广泛,无法完全包含在内,这是为了理解如何实现异常的基本思想而简化的实现。说它发出0个运行时指令是误导性的。例如,一个简单的ClassCastException扩展了RuntimeException,它又扩展了Exception,它又扩展了Throwable,涉及到:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7-b147/java/lang/Throwable.java... 这就像说C中的switch-case是免费的,如果只使用1个case,仍然存在一些小的启动开销。 - technosaurus
1
@Patashu 所有这些预编译的位都必须在启动时加载,无论它们是否被使用。在编译时无法知道运行时是否会出现内存不足异常 - 这就是为什么它们被称为运行时异常 - 否则它们将是编译器警告/错误,因此它并没有优化掉所有东西,处理它们的所有代码都包含在编译后的代码中,并具有启动成本。 - technosaurus
2
我不太了解 C 语言。在 C# 和 Java 中,try 块是通过添加元数据而不是代码来实现的。当进入 try 块时,没有任何执行指示 - 当抛出异常时,堆栈将被展开并检查该异常类型的处理程序的元数据(代价高昂)。 - Patashu
1
是的,我实际上已经实现了一个Java解释器和静态字节码编译器,并且在随后的JITC(针对IBM iSeries)上工作过,我可以告诉你,在字节码中没有任何东西“标记”try范围的进入/退出,而是在单独的表格中识别范围。解释器对于try范围不做任何特殊处理(直到引发异常)。JITC(或静态字节码编译器)必须意识到边界以抑制先前提到的优化。 - Hot Licks
显示剩余3条评论

5

我发现捕获NullPointException的代价相当昂贵。对于1.2k次操作,时间约为200毫秒,但当我采用同样的方式处理时,即if(object==null),时间减少到了12毫秒,这对我来说是相当大的改进。


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