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


189

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

我发现了堆栈溢出问题/答案为什么尝试块价格昂贵?,但适用于.NET


30
这个问题真的没有意义。Try..catch具有非常特定的目的。如果需要,就需要。无论如何,没有成功的尝试有什么意义?
JohnFx

49
try { /* do stuff */ } finally { /* make sure to release resources */ }是合法且有用的
-A4L

4
必须权衡成本与收益。它并不孤单。无论如何,昂贵是相对的,并且直到您知道自己无法做到这一点,才有可能使用最明显的方法而不是不做某事,因为这样做可能会在一小时的时间内节省您一到两毫秒的时间。程序执行。
乔尔

4
我希望这不会导致“让我们重新发明错误代码”类型的情况……
–mikołak,

6
@SAFX:使用Java7,您甚至可以finally使用try-with-resources
a_horse_with_no_name

Answers:


201

try几乎没有任何花销。try代码的元数据不是在运行时进行设置,而是在编译时进行结构化,这样,当引发异常时,它现在执行相对昂贵的操作,即遍历堆栈并查看是否try存在任何块可以捕获此异常。例外。从外行的角度来看,它try可能也是免费的。它实际上是在抛出导致您付出代价的异常-但是,除非您抛出数百或数千个异常,否则您仍然不会注意到成本。


try有一些与此相关的小费用。Java无法对代码try块中的代码进行其他优化,而在其他方面则无法做到。例如,Java经常会重新安排方法中的指令以使其运行更快-但是Java还需要保证,如果引发异常,则将观察该方法的执行,就像执行源代码中编写的语句一样为了达到某条线。

因为在一个try块中可以抛出一个异常(在try块的任何行上!有些异常是异步抛出的,例如通过调用stop一个Thread(已弃用),甚至OutOfMemoryError几乎可以在任何地方发生),但是它可以捕获并以相同的方法继续执行代码,因此很难对可以进行的优化进行推理,因此不太可能发生优化。(有些人必须对编译器进行编程,以进行推理,保证正确性等。对于要成为“例外”的东西,这将是一个巨大的痛苦。)但是,实际上,您不会注意到这样的事情。


2
一些异常是异步引发的,它们不是异步的,而是引发安全点的。而这部分尝试会带来一些小成本。Java无法对try块中的代码进行某些优化,否则它需要认真参考。在某些时候,代码很可能在try / catch块中。可能确实很难插入try / catch块,并且很难为结果建立适当的晶格,但是带有重排的部分却模棱两可。
bestss13年

2
没有try...finallycatch也会阻止一些优化吗?
dajood 2015年

5
@Patashu “实际上是在抛出使您付出代价的异常”从技术上讲,抛出异常并不昂贵;实例化Exception对象是大多数时间的过程。
奥斯汀D

72

我们来衡量一下吧?

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 所说:“过早的优化是万恶之源”。


4
尽管大多数JVM上的try / no try很有可能是相同的,但微基准测试存在严重缺陷。
bestsss

2
相当多的级别:您的意思是计算结果的时间不到1ns?编译后的代码将同时删除try / catch和循环(从1到n的总和是微不足道的算术级数和)。即使代码包含try / finally,编译器也可以证明,没有东西可以扔在那里。抽象代码只有两个调用站点,它将被克隆并内联。在更多情况下,只需查找有关微基准的一些文章,然后您决定编写微基准始终检查生成的程序集。
2013年

3
报告的时间是循环每次迭代。因为只有在总经过时间> 0.1秒(或20亿次迭代,在这里不是这种情况)时才使用度量,所以我发现您断言该循环已被完全删除-因为如果删除了循环,花了0.1秒执行了什么?
Meriton

...的确,根据-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly,实际上,在生成的本机代码中同时存在循环和循环。不,抽象方法没有内联,因为它们的调用者不是及时地编译的(大概是因为没有被足够多次地调用)。
Meriton

如何在Java中编写正确的微基准测试:stackoverflow.com/questions/504103/…–
Vadzim

46

try/ catch可能会对性能产生影响。这是因为它阻止JVM进行一些优化。约书亚·布洛赫(Joshua Bloch)在《有效的Java》中说:

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


24
“它阻止JVM进行一些优化” ...?您能详细说明吗?
海妖

5
例如,不能使用try块外的代码对@try块内的Kraken代码(通常?总是?总是?)进行重新排序。
Patashu

3
请注意,问题是“它是否昂贵”,而不是“它是否对性能有影响”。
mikołak

3
添加了来自有效Java的摘录,这当然是Java的圣经;除非有参考,否则摘录不告诉任何内容。实际上,Java中的任何代码都在某种程度上处于try / final范围内。
bestsss

29

是的,正如其他人所说的那样,一个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不能保证第一个表达式曾经被计算过,因此必须重新计算该表达式。在“直线”代码中,这通常不是什么大问题,但在循环中可能很重要。

但是应注意,这仅适用于JITCed代码。javac仅进行少量优化,字节码解释器进入/离开try块的成本为零。(没有生成字节码来标记块边界。)

并且最好的:

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帧中。
2013年

@bestsss-即使生成(不是给定的)GOTO,其代价也很小,而且远不是块边界的“标记”-可以为许多构造生成GOTO。
热舔

我从未提到过成本,但是没有生成的字节码是错误的语句。就这样。实际上,字节码中没有块,帧不等于块。
2013年

如果尝试直接落入最后,将不会有GOTO,并且在其他情况下将不会有GOTO。关键是“输入尝试” /“退出尝试”字节码的顺序不存在。
热舔

如果尝试直接落入最后,将不会有GOTO-错误!finally字节码中没有,try/catch(Throwable any){...; throw any;}它的确有catch语句w / frame和Throwable必须定义(非null),依此类推。您为什么要争论这个话题,您至少可以检查一些字节码?展示的当前指南。最后的方法是复制块并避免使用goto节(先前的impl),但是必须根据字节数来复制字节码。
bestsss

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
您为try / catch找到的这些C宏与Java或C#实现并不等效,它们发出0条运行时指令。
Patashu

Java实现过于广泛而无法完整包含,这是一种简化的实现,目的是理解如何实现异常的基本思想。说它发出0个运行时指令是一种误导。例如,一个简单的classcastexception扩展了runtimeexception,它扩展了扩展throwable的异常,其中涉及:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk / ... ... ...就像说C中的切换情况是如果只使用一种情况,则为免费,则启动开销仍然很小。

1
@Patashu无论是否使用过,所有这些预编译位都必须在启动时加载。没有办法知道在编译时的运行时是否会发生内存不足的异常-这就是为什么将它们称为运行时异常-否则它们将是编译器警告/错误,因此,它不能使所有内容都得到优化,所有处理它们的代码都包含在已编译的代码中,并且具有启动成本。

2
我不会说C。在C#和Java中,尝试是通过添加元数据而不是代码来实现的。输入try块后,将不执行任何操作来表明这一点-引发异常时,展开堆栈并检查元数据以查找该异常类型(昂贵)的处理程序。
Patashu

1
是的,我实际上已经实现了Java解释器和静态字节码编译器,并在后续的JITC(适用于IBM iSeries)上进行了工作,我可以告诉您在字节码中没有“标记” try范围的输入/退出的东西,而是范围在单独的表格中标识。解释器对try范围不做任何特殊操作(直到引发异常)。JITC(或静态字节码编译器)必须了解边界,才能抑制优化,如前所述。
热舔

8

另一个微基准测试(来源)。

我创建了一个测试,在该测试中,我基于异常百分比来评估try-catch和no-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   

也就是说,这些情况之间没有显着差异。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.