我们知道捕获异常的成本很高。但是,即使从未抛出异常,在 Java 中使用 try-catch 块是否也很昂贵?
我找到了 Stack Overflow 问题/答案Why are try blocks expensive?,但它适用于 .NET。
try { /* do stuff */ } finally { /* make sure to release resources */ }
合法且有用
try-with-resources
摆脱 finally
块
try
几乎没有任何费用。代码的元数据不是在运行时设置 try
的工作,而是在编译时构建的,这样当抛出异常时,它现在执行一个相对昂贵的操作,即向上走栈并查看是否有 try
存在会捕获此异常的块。从外行的角度来看,try
也可能是免费的。它实际上是抛出了让你付出代价的异常——但除非你抛出数百或数千个异常,否则你仍然不会注意到成本。
try
有一些与之相关的小成本。 Java 无法对 try
块中的代码进行一些优化,否则它会这样做。例如,Java 经常会重新安排方法中的指令以使其运行得更快——但 Java 还需要保证,如果抛出异常,该方法的执行被观察到就好像其在源代码中编写的语句被执行一样为了达到某些线。
因为在 try
块中可以抛出异常(在 try 块中的任何行!一些异常是异步抛出的,例如通过在 Thread 上调用 stop
(已弃用),甚至可能发生 OutOfMemoryError几乎任何地方),但它可以被捕获并且代码在之后以相同的方法继续执行,更难以推理可以进行的优化,因此它们不太可能发生。 (有人必须对编译器进行编程来执行它们,推理并保证正确性等。对于意味着“异常”的东西来说,这将是一个很大的痛苦)但同样,在实践中你不会注意到这样的事情。
让我们测量一下,好吗?
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 put 所说:“过早的优化是万恶之源”。
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
,循环和其中的添加都存在于生成的本机代码中。不,抽象方法没有内联,因为它们的调用者不只是及时编译(大概是因为它没有被调用足够多次)。
try
/catch
可能会对性能产生一些影响。这是因为它阻止了 JVM 进行一些优化。 Joshua Bloch 在“Effective Java”中说:
• 将代码放在try-catch 块中会抑制现代JVM 实现可能执行的某些优化。
是的,正如其他人所说,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
}
没有“转到”。
catch/finally
帧。
finally
,它是 try/catch(Throwable any){...; throw any;}
,并且它确实有带有帧的 catch 语句和必须定义的 Throwable(非 null)等等。为什么你试图争论这个话题,你至少可以检查一些字节码? impl的当前指南。最后是复制块并避免 goto 部分(以前的 impl),但是必须根据有多少出口点来复制字节码。
另一个微基准 (source)。
我创建了一个测试,在其中我根据异常百分比测量 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
Percentage | Result (try/if, ns) 0% | 88/90 1% | 89/87 10% | 86/97 90% | 85/83
这表明这些案例之间没有显着差异。
要了解为什么无法执行优化,了解底层机制很有用。我能找到的最简洁的例子是在 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,因此他们会跳过无法保证安全的优化,但实现本身相当轻松。
我发现捕获 NullPointException 非常昂贵。对于 1.2k 操作,当我使用 if(object==null)
以相同方式处理它时,时间分别为 200 毫秒和 12 毫秒,这对我来说是相当大的改进。
catch
的try...finally
块是否也会阻止某些优化?Exception
对象是大部分时间。