ChatGPT解决这个技术问题 Extra ChatGPT

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

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

我找到了 Stack Overflow 问题/答案Why are try blocks expensive?,但它适用于 .NET

这个问题真的没有意义。 Try..catch 有一个非常具体的目的。如果你需要它,你需要它。无论如何,没有捕获的尝试有什么意义?
try { /* do stuff */ } finally { /* make sure to release resources */ } 合法且有用
必须权衡成本与收益。它并不孤单。无论如何,昂贵是相对的,在你知道自己做不到之前,使用最明显的方法而不是不做某事是有意义的,因为它可能会在一小时内为你节省一两毫秒程序执行。
我希望这不会导致“让我们重新发明错误代码”类型的情况......
@SAFX:使用 Java7,您甚至可以使用 try-with-resources 摆脱 finally

P
Patashu

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

try 有一些与之相关的小成本。 Java 无法对 try 块中的代码进行一些优化,否则它会这样做。例如,Java 经常会重新安排方法中的指令以使其运行得更快——但 Java 还需要保证,如果抛出异常,该方法的执行被观察到就好像其在源代码中编写的语句被执行一样为了达到某些线。

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


有些异常是异步抛出的,它们不是异步的,而是在安全点抛出的。这部分尝试有一些与之相关的小成本。 Java 无法对 try 块中的代码进行一些优化,否则它确实需要认真参考。在某些时候,代码很可能在 try/catch 块中。可能确实,try/catch 块更难内联并为结果构建适当的晶格,但重新排列的部分是模棱两可的。
没有 catchtry...finally 块是否也会阻止某些优化?
@Patashu “实际上是抛出异常让你付出代价” 从技术上讲,抛出异常并不昂贵;实例化 Exception 对象是大部分时间。
m
meriton

让我们测量一下,好吗?

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


虽然在大多数 JVM 上尝试/不尝试很可能是相同的,但微基准测试存在严重缺陷。
相当多的级别:您的意思是结果是在 1ns 内计算的?编译后的代码将完全删除 try/catch 和循环(从 1 到 n 的数字相加是一个微不足道的算术级数和)。即使代码包含编译器可以证明的 try/finally,也没有什么可以扔进去的。抽象代码只有 2 个调用站点,它将被克隆和内联。还有更多的情况,只要查一些关于 microbenchmark 的文章,如果你决定写一个 microbenchmark 总是检查生成的程序集。
报告的时间是循环的每次迭代。因为只有在总经过时间 > 0.1 秒(或 20 亿次迭代,这里不是这种情况)时才会使用测量,我发现你断言循环已被完全删除很难相信 - 因为如果循环被删除了,什么需要 0.1 秒来执行?
...事实上,根据 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly,循环和其中的添加都存在于生成的本机代码中。不,抽象方法没有内联,因为它们的调用者不只是及时编译(大概是因为它没有被调用足够多次)。
如何在 Java 中编写正确的微基准测试:stackoverflow.com/questions/504103/…
E
Evgeniy Dorofeev

try/catch 可能会对性能产生一些影响。这是因为它阻止了 JVM 进行一些优化。 Joshua Bloch 在“Effective Java”中说:

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


“它阻止 JVM 进行一些优化”......?你能详细说明一下吗?
@The Kraken 代码在 try 块内(通常?总是?)不能用 try 块外的代码重新排序,例如。
请注意,问题是“它是否昂贵”,而不是“它是否对性能有任何影响”。
添加了来自 Effective Java 的摘录,这当然是 Java 的圣经;除非有参考,否则摘录不会说明任何事情。实际上,java 中的任何代码都在 try/finally 范围内。
k
kevinarpe

是的,正如其他人所说,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 离开块,否则它将落入 catch/finally 帧。
@bestsss - 即使生成了 GOTO(不是给定的),其成本也是微不足道的,而且它远不是块边界的“标记”——可以为许多构造生成 GOTO。
我从来没有提到成本,但是没有生成的字节码是一个错误的陈述。就这样。实际上,字节码中没有块,帧不等于块。
如果try直接落入finally就不会出现GOTO,还有其他场景不会出现GOTO。关键是“输入尝试”/“退出尝试”字节码的顺序没有任何内容。
如果 try 直接落入 finally,则不会有 GOTO - False!字节码中没有 finally,它是 try/catch(Throwable any){...; throw any;},并且它确实有带有帧的 catch 语句和必须定义的 Throwable(非 null)等等。为什么你试图争论这个话题,你至少可以检查一些字节码? impl的当前指南。最后是复制块并避免 goto 部分(以前的 impl),但是必须根据有多少出口点来复制字节码。
A
Andrey Chaschev

另一个微基准 (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

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


t
technosaurus

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


您为 try/catch 找到的这些 C 宏并不等同于 Java 或 C# 实现,后者发出 0 个运行时指令。
java 实现过于广泛,无法完整包含,这是一个简化的实现,目的是了解如何实现异常的基本思想。说它发出 0 个运行时指令是一种误导。例如,一个简单的 classcastexception 扩展 runtimeexception 扩展异常,该异常扩展 throwable 涉及:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ...这就像说 C 中的 switch-case 是免费的,如果只使用过 1 个 case,仍然有一个小的启动开销。
@Patashu 所有这些预编译位仍然必须在启动时加载,无论它们是否曾经使用过。没有办法知道在编译时运行时是否会出现内存不足异常 - 这就是为什么它们被称为运行时异常 - 否则它们将是编译器警告/错误,所以不,它不会优化一切,所有处理它们的代码都包含在编译代码中,并且有启动成本。
我不能谈论 C。在 C# 和 Java 中,try 是通过添加元数据而不是代码来实现的。当进入 try 块时,不会执行任何操作来表明这一点 - 当抛出异常时,堆栈会展开并检查元数据以查找该异常类型的处理程序(昂贵)。
是的,我实际上已经实现了一个 Java 解释器和静态字节码编译器,并在后续的 JITC(用于 IBM iSeries)上工作,我可以告诉你,没有什么可以“标记”字节码中尝试范围的进入/退出,而是范围在单独的表中标识。解释器对尝试范围没有什么特别的作用(直到引发异常)。 JITC(或静态字节码编译器)必须知道边界以抑制前面所述的优化。
M
Mateusz Kaflowski

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