抛出异常的哪一部分是昂贵的?

Mar*_*ney 252 java performance exception

在Java中,当实际上没有错误时使用throw/catch作为逻辑的一部分通常是一个坏主意(部分)因为抛出和捕获异常是昂贵的,并且在循环中多次执行它通常比其他更慢控制结构,不涉及抛出异常.

我的问题是,抛出/捕获本身或创建Exception对象时产生的成本(因为它获得了很多运行时信息,包括执行堆栈)?

换句话说,如果我这样做

Exception e = new Exception();
Run Code Online (Sandbox Code Playgroud)

但是不要扔它,是投掷的大部分成本,还是投掷+捕获处理成本高昂?

我不是在将代码放入try/catch块中是否会增加执行代码的成本,我问的是,捕获Exception是否是昂贵的部分,或者创建(调用构造函数)Exception是否是昂贵的部分.

另一种问这个问题的方法是,如果我创建了一个Exception实例并且一遍又一遍地抛出它,那么每次抛出时创建一个新的Exception会明快得多吗?

apa*_*gin 256

创建异常对象并不比创建其他常规对象更昂贵.主要成本隐藏在本机fillInStackTrace方法中,该方法遍历调用堆栈并收集构建堆栈跟踪所需的所有信息:类,方法名称,行号等.

关于高异常成本的神话来自于大多数Throwable构造函数隐式调用的事实fillInStackTrace.但是,有一个构造函数可以创建Throwable没有堆栈跟踪的构造函数.它允许您制作非常快速实例化的throwable.创建轻量级异常的另一种方法是覆盖fillInStackTrace.


那么抛出异常怎么样?
实际上,它取决于捕获抛出异常的位置.

如果它被捕获在相同的方法中(或者更确切地说,在相同的上下文中,因为上下文可以包括由于内联的几个方法),那么throw就像goto(当然,在JIT编译之后)一样快速和简单.

但是,如果catch块位于堆栈的更深处,那么JVM需要展开堆栈帧,这可能需要更长的时间.如果synchronized涉及到块或方法,则需要更长的时间,因为展开意味着释放由移除的堆栈帧拥有的监视器.


我可以通过适当的基准来确认上述陈述,但幸运的是我不需要这样做,因为HotSpot的性能工程师Alexey Shipilev的帖子已经完全涵盖了所有方面:Lil'例外的性能.

  • 量化异常的开销可能是值得的.即使是在这篇相当详尽的文章中报告的最坏情况(使用实际查询的堆栈跟踪投掷和捕获动态异常,深度为1000个堆栈帧),需要80微秒.如果您的系统需要每秒处理数千个异常,那么这可能很重要,但是否则不值得担心.这是最糟糕的情况; 如果你的堆栈跟踪有点理智,或者你不查询它们堆栈跟踪,我们每秒可以处理近百万个异常. (14认同)
  • 我强调这一点是因为许多人在阅读异常"昂贵"之后,永远不会停下来问"与什么相比价格昂贵",但他们认为它们是"程序中昂贵的一部分",而这些很少见. (13认同)
  • 正如文章中所提到的那样,结果是投掷/捕获异常的成本在很大程度上取决于调用的深度.这里的要点是"例外很昂贵"的陈述并不是真的正确.更正确的说法是,例外"可能"很昂贵.老实说,我认为只说"真正例外情况"的例外情况(如文章中所述)措辞过于强硬.它们非常适用于正常回流之外的任何事物,并且很难检测在实际应用中以这种方式使用它们的性能影响. (8认同)
  • @MatthieuM.异常和try/catch块不会阻止JVM内联.对于编译方法,从存储为元数据的虚拟堆栈帧表重建实际堆栈跟踪.我不记得与try/catch不兼容的JIT优化.Try/catch结构本身不会向方法代码添加任何内容,它仅作为代码之外的异常表存在. (3认同)
  • 这里没有提到一部分:阻止优化应用的潜在成本.一个极端的例子是JVM没有内联以避免"混乱"堆栈跟踪,但我已经看到(微)基准,其中异常的存在或不存在会导致或破坏C++中的优化. (2认同)
  • @ o11c与静态编译器不同,JVM非常擅长推测优化.它可能会说'好吧,在分析阶段没有看到任何异常,所以让我们编译这段代码就好像这里没有try/catch一样'.如果在此代码中发生异常,JVM将对其进行去优化,并回退到解释器,以便使用新知识进一步重新编译.我承认可能存在一些人工案例,其中try/catch导致更糟糕的代码生成,但这不太可能是常见的情况. (2认同)

eri*_*son 72

大多数Throwable构造函数中的第一个操作是填充堆栈跟踪,这是大部分费用的来源.

但是,有一个受保护的构造函数,其中包含一个禁用堆栈跟踪的标志.扩展时也可以访问此构造函数Exception.如果创建自定义异常类型,则可以避免创建堆栈跟踪并以更少的信息为代价获得更好的性能.

如果通过常规方法创建任何类型的单个异常,则可以多次重新抛出它,而无需填充堆栈跟踪的开销.但是,它的堆栈跟踪将反映它的构造位置,而不是它在特定实例中抛出的位置.

当前版本的Java尝试优化堆栈跟踪创建.调用本机代码以填充堆栈跟踪,该跟踪以较轻的本机结构记录跟踪.StackTraceElement只有在调用需要跟踪的方法或其他方法时getStackTrace(),才会从此记录中延迟创建相应的Java 对象printStackTrace().

如果消除堆栈跟踪生成,另一个主要成本是在throw和catch之间展开堆栈.在捕获异常之前遇到的中间帧越少,这将越快.

设计您的程序,以便仅在真正特殊情况下抛出异常,并且这些优化很难证明.

  • 链接到构造函数:https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html#Exception-java.lang.String-java.lang.Throwable-boolean-boolean- (3认同)

Har*_*rry 25

这里有关于例外的好文章.

http://shipilev.net/blog/2014/exceptional-performance/

结论是堆栈跟踪结构和堆栈展开是昂贵的部分.下面的代码利用了1.7我们可以打开和关闭堆栈跟踪的功能.然后我们可以使用它来查看不同场景的成本

以下是单独创建对象的时间.我已经添加String到这里,所以你可以看到,没有写入堆栈,创建一个JavaExceptionObject和一个几乎没有区别String.随着堆叠写入开启,差异是显着的,即至少慢一个数量级.

Time to create million String objects: 41.41 (ms)
Time to create million JavaException objects with    stack: 608.89 (ms)
Time to create million JavaException objects without stack: 43.50 (ms)
Run Code Online (Sandbox Code Playgroud)

以下显示了从特定深度的投掷返回一百万次所需的时间.

|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
|   16|           1428|             243| 588 (%)|
|   15|           1763|             393| 449 (%)|
|   14|           1746|             390| 448 (%)|
|   13|           1703|             384| 443 (%)|
|   12|           1697|             391| 434 (%)|
|   11|           1707|             410| 416 (%)|
|   10|           1226|             197| 622 (%)|
|    9|           1242|             206| 603 (%)|
|    8|           1251|             207| 604 (%)|
|    7|           1213|             208| 583 (%)|
|    6|           1164|             206| 565 (%)|
|    5|           1134|             205| 553 (%)|
|    4|           1106|             203| 545 (%)|
|    3|           1043|             192| 543 (%)| 
Run Code Online (Sandbox Code Playgroud)

以下几乎可以肯定是过度简化......

如果我们在堆栈写入时采用16的深度,那么对象创建大约需要大约40%的时间,实际的堆栈跟踪占据了绝大多数.实例化JavaException对象的~93%是由于采用了堆栈跟踪.这意味着在这种情况下展开堆栈占用了其他50%的时间.

当我们关闭堆栈跟踪对象创建帐户的小得多,即20%,堆栈展开现在占80%的时间.

在这两种情况下,堆栈展开占用了总时间的很大一部分.

public class JavaException extends Exception {
  JavaException(String reason, int mode) {
    super(reason, null, false, false);
  }
  JavaException(String reason) {
    super(reason);
  }

  public static void main(String[] args) {
    int iterations = 1000000;
    long create_time_with    = 0;
    long create_time_without = 0;
    long create_string = 0;
    for (int i = 0; i < iterations; i++) {
      long start = System.nanoTime();
      JavaException jex = new JavaException("testing");
      long stop  =  System.nanoTime();
      create_time_with += stop - start;

      start = System.nanoTime();
      JavaException jex2 = new JavaException("testing", 1);
      stop = System.nanoTime();
      create_time_without += stop - start;

      start = System.nanoTime();
      String str = new String("testing");
      stop = System.nanoTime();
      create_string += stop - start;

    }
    double interval_with    = ((double)create_time_with)/1000000;
    double interval_without = ((double)create_time_without)/1000000;
    double interval_string  = ((double)create_string)/1000000;

    System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string);
    System.out.printf("Time to create %d JavaException objects with    stack: %.2f (ms)\n", iterations, interval_with);
    System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without);

    JavaException jex = new JavaException("testing");
    int depth = 14;
    int i = depth;
    double[] with_stack    = new double[20];
    double[] without_stack = new double[20];

    for(; i > 0 ; --i) {
      without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000;
      with_stack[i]    = jex.timerLoop(i, iterations, 1)/1000000;
    }
    i = depth;
    System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n");
    for(; i > 0 ; --i) {
      double ratio = (with_stack[i] / (double) without_stack[i]) * 100;
      System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio);
      //System.out.printf("%d\t%.2f (ms)\n", i, ratio);
    }
  }
 private int thrower(int i, int mode) throws JavaException {
    ExArg.time_start[i] = System.nanoTime();
    if(mode == 0) { throw new JavaException("without stack", 1); }
    throw new JavaException("with stack");
  }
  private int catcher1(int i, int mode) throws JavaException{
    return this.stack_of_calls(i, mode);
  }
  private long timerLoop(int depth, int iterations, int mode) {
    for (int i = 0; i < iterations; i++) {
      try {
        this.catcher1(depth, mode);
      } catch (JavaException e) {
        ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]);
      }
    }
    //long stop = System.nanoTime();
    return ExArg.time_accum[depth];
  }

  private int bad_method14(int i, int mode) throws JavaException  {
    if(i > 0) { this.thrower(i, mode); }
    return i;
  }
  private int bad_method13(int i, int mode) throws JavaException  {
    if(i == 13) { this.thrower(i, mode); }
    return bad_method14(i,mode);
  }
  private int bad_method12(int i, int mode) throws JavaException{
    if(i == 12) { this.thrower(i, mode); }
    return bad_method13(i,mode);
  }
  private int bad_method11(int i, int mode) throws JavaException{
    if(i == 11) { this.thrower(i, mode); }
    return bad_method12(i,mode);
  }
  private int bad_method10(int i, int mode) throws JavaException{
    if(i == 10) { this.thrower(i, mode); }
    return bad_method11(i,mode);
  }
  private int bad_method9(int i, int mode) throws JavaException{
    if(i == 9) { this.thrower(i, mode); }
    return bad_method10(i,mode);
  }
  private int bad_method8(int i, int mode) throws JavaException{
    if(i == 8) { this.thrower(i, mode); }
    return bad_method9(i,mode);
  }
  private int bad_method7(int i, int mode) throws JavaException{
    if(i == 7) { this.thrower(i, mode); }
    return bad_method8(i,mode);
  }
  private int bad_method6(int i, int mode) throws JavaException{
    if(i == 6) { this.thrower(i, mode); }
    return bad_method7(i,mode);
  }
  private int bad_method5(int i, int mode) throws JavaException{
    if(i == 5) { this.thrower(i, mode); }
    return bad_method6(i,mode);
  }
  private int bad_method4(int i, int mode) throws JavaException{
    if(i == 4) { this.thrower(i, mode); }
    return bad_method5(i,mode);
  }
  protected int bad_method3(int i, int mode) throws JavaException{
    if(i == 3) { this.thrower(i, mode); }
    return bad_method4(i,mode);
  }
  private int bad_method2(int i, int mode) throws JavaException{
    if(i == 2) { this.thrower(i, mode); }
    return bad_method3(i,mode);
  }
  private int bad_method1(int i, int mode) throws JavaException{
    if(i == 1) { this.thrower(i, mode); }
    return bad_method2(i,mode);
  }
  private int stack_of_calls(int i, int mode) throws JavaException{
    if(i == 0) { this.thrower(i, mode); }
    return bad_method1(i,mode);
  }
}

class ExArg {
  public static long[] time_start;
  public static long[] time_accum;
  static {
     time_start = new long[20];
     time_accum = new long[20];
  };
}
Run Code Online (Sandbox Code Playgroud)

与您通常找到的相比,此示例中的堆栈帧很小.

你可以使用javap查看字节码

javap -c -v -constants JavaException.class
Run Code Online (Sandbox Code Playgroud)

即这是方法4 ...

   protected int bad_method3(int, int) throws JavaException;
flags: ACC_PROTECTED
Code:
  stack=3, locals=3, args_size=3
     0: iload_1       
     1: iconst_3      
     2: if_icmpne     12
     5: aload_0       
     6: iload_1       
     7: iload_2       
     8: invokespecial #6                  // Method thrower:(II)I
    11: pop           
    12: aload_0       
    13: iload_1       
    14: iload_2       
    15: invokespecial #17                 // Method bad_method4:(II)I
    18: ireturn       
  LineNumberTable:
    line 63: 0
    line 64: 12
  StackMapTable: number_of_entries = 1
       frame_type = 12 /* same */

Exceptions:
  throws JavaException
Run Code Online (Sandbox Code Playgroud)


Aus*_*n D 12

Exception使用null堆栈跟踪创建所需的时间throwtry-catch阻塞在一起的时间大致相同.但是,填充堆栈跟踪的时间平均要长5倍.

我创建了以下基准来演示对性能的影响.我添加了-Djava.compiler=NONE"运行配置"以禁用编译器优化.为了衡量构建堆栈跟踪的影响,我扩展了Exception类以利用无堆栈构造函数:

class NoStackException extends Exception{
    public NoStackException() {
        super("",null,false,false);
    }
}
Run Code Online (Sandbox Code Playgroud)

基准代码如下:

public class ExceptionBenchmark {

    private static final int NUM_TRIES = 100000;

    public static void main(String[] args) {

        long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0;

        for (int i = 0; i < 30; i++) {
            throwCatchTime += throwCatchLoop();
            newExceptionTime += newExceptionLoop();
            newObjectTime += newObjectLoop();
            noStackExceptionTime += newNoStackExceptionLoop();
        }

        System.out.println("throwCatchTime = " + throwCatchTime / 30);
        System.out.println("newExceptionTime = " + newExceptionTime / 30);
        System.out.println("newStringTime = " + newObjectTime / 30);
        System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30);

    }

    private static long throwCatchLoop() {
        Exception ex = new Exception(); //Instantiated here
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            try {
                throw ex; //repeatedly thrown
            } catch (Exception e) {

                // do nothing
            }
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Exception e = new Exception();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newObjectLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            Object o = new Object();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

    private static long newNoStackExceptionLoop() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < NUM_TRIES; i++) {
            NoStackException e = new NoStackException();
        }
        long stop = System.currentTimeMillis();
        return stop - start;
    }

}
Run Code Online (Sandbox Code Playgroud)

输出:

throwCatchTime = 19
newExceptionTime = 77
newObjectTime = 3
noStackExceptionTime = 15
Run Code Online (Sandbox Code Playgroud)

这意味着创建a NoStackException与重复抛出它一样昂贵Exception.它还表明,创建Exception并填充其堆栈跟踪需要大约4倍的时间.