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

我们知道捕获异常的代价很高。但是,即使从未抛出异常,在Java中使用try-catch块的代价也很高吗?

我发现Stack Overflow问题/答案为什么try块很贵?< / >,但它是为net

55370 次浏览

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

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

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


try有一些与它相关的小成本。Java不能对try块中的代码做一些它本来可以做的优化。例如,Java经常会重新安排方法中的指令,以使其运行得更快——但Java也需要保证,如果抛出异常,方法的执行就会被观察到,就像源代码中所写的语句一样,按照直到某一行的顺序执行。

因为在try块中可以抛出异常(在try块中的任何一行!)有些异常是异步抛出的,比如通过在线程上调用stop(已弃用),甚至除了OutOfMemoryError几乎可以在任何地方发生之外),但它可以被捕获,然后代码继续在相同的方法中执行,很难推断可以进行的优化,所以它们不太可能发生。(必须有人编程编译器来执行这些操作,推理并保证正确性,等等。对于一些“特殊”的东西来说,这将是一个巨大的痛苦)但同样,在实践中你不会注意到这样的事情。

是的,正如其他人所说,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
}

没有“转到”。

我们来量一下,好吗?

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

要理解为什么不能执行优化,了解底层机制是有用的。我能找到的最简洁的例子是在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,所以他们跳过优化,他们不能保证安全,但实现本身相当轻。

另一个微基准测试()。

我创建了一个测试,在这个测试中,我根据异常百分比来测量尝试捕获和不尝试捕获的代码版本。10%百分比意味着10%的测试用例除以零用例。在一种情况下,它由try-catch块处理,在另一种情况下由条件操作符处理。这是我的结果表:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Percentage | Result (try/if, ns)
0%     |      88/90
1%     |      89/87
10%    |      86/97
90%    |      85/83

这表明这些案例之间没有显著差异。

我发现捕获NullPointException非常昂贵。对于1.2k操作,当我用if(object==null)以同样的方式处理它时,时间是200ms和12ms,这对我来说是很大的改进。