【问题标题】:Which part of throwing an Exception is expensive?抛出异常的哪一部分是昂贵的?
【发布时间】:2016-07-20 11:39:34
【问题描述】:

在 Java 中,在实际上没有错误的情况下使用 throw/catch 作为逻辑的一部分通常是一个坏主意(部分),因为抛出和捕获异常的成本很高,而且在循环中多次执行通常很远比其他不涉及抛出异常的控制结构慢。

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

换句话说,如果我这样做了

Exception e = new Exception();

但不要扔,这是扔的大部分成本,还是扔 + 接处理的成本高?

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

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

【问题讨论】:

  • 我相信它正在填充和填充堆栈跟踪。
  • “如果我创建了一个 Exception 实例并一遍又一遍地抛出并捕获它,”当创建异常时,它的堆栈跟踪被填充,这意味着它总是相同的 stacttrace,无论它是从哪里抛出的.如果堆栈跟踪对您来说并不重要,那么您可以尝试您的想法,但在某些情况下,如果不是不可能的话,这可能会使调试变得非常困难。
  • @Pshemo 我不打算实际上在代码中这样做,我在询问性能,并以这种荒谬为例,它可能会有所作为.
  • @MartinCarney 我已经为您的最后一段添加了答案,即缓存异常会提高性能。如果有用我可以添加代码,如果没有我可以删除答案。

标签: java performance exception


【解决方案1】:

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

大多数Throwable 构造函数都隐式调用fillInStackTrace。这就是创建异常缓慢的想法的来源。但是,有一个constructor 可以创建一个没有堆栈跟踪的Throwable。它允许您制作可快速实例化的 throwable。创建轻量级异常的另一种方法是覆盖fillInStackTrace


现在抛出异常怎么办?
事实上,这取决于抛出的异常在哪里捕获

如果它在同一个方法中被捕获(或者更准确地说,在同一个上下文中,由于内联,上下文可以包含多个方法),那么throwgoto 一样快速和简单(当然, JIT 编译后)。

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


我可以通过适当的基准来确认上述陈述,但幸运的是我不需要这样做,因为所有方面都已经在 HotSpot 的性能工程师 Alexey Shipilev 的帖子中完美涵盖:The Exceptional Performance of Lil' Exception

【讨论】:

  • 如文章中所述并在此处提及,结果是抛出/捕获异常的成本很大程度上取决于调用的深度。这里的重点是“例外是昂贵的”这句话并不真正正确。更正确的说法是异常“可能”代价高昂。老实说,我认为只对“真正的例外情况”(如文章中的)使用例外的措辞过于强硬。它们非常适合正常回流之外的几乎任何事情,并且很难检测在实际应用程序中以这种方式使用它们对性能的影响。
  • 量化异常的开销可能是值得的。即使在这篇相当详尽的文章中报告的最糟糕的情况下(抛出并捕获一个动态异常,堆栈跟踪实际上是查询的,1000 个堆栈帧深),也需要 80 微秒。如果您的系统每秒需要处理数千个异常,这可能很重要,但否则不值得担心。这是最坏的情况;如果您的堆栈跟踪更加理智,或者您不查询堆栈跟踪,我们可以每秒处理近一百万个异常。
  • 我强调这一点是因为很多人在读到例外是“昂贵的”时,永远不会停下来问“与什么相比昂贵”,而是假设它们是“他们程序的昂贵部分”,他们很少有。
  • 这里没有提到一个部分:阻止优化应用的潜在成本。一个极端的例子是 JVM 没有内联以避免“混淆”堆栈跟踪,但我已经看到(微)基准,其中异常的存在或不存在会影响 C++ 中的优化。
  • @MatthieuM。异常和 try/catch 块不会阻止 JVM 内联。对于已编译的方法,实际堆栈跟踪是从存储为元数据的虚拟堆栈帧表重建的。我不记得与 try/catch 不兼容的 JIT 优化。 Try/catch 结构本身并没有给方法代码添加任何东西,它只是作为代码之外的异常表存在。
【解决方案2】:

大多数Throwable 构造函数中的第一个操作是到fill in the stack trace,,这是大部分费用所在。

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

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

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

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

设计您的程序,以便仅在真正异常的情况下引发异常,而这样的优化很难证明是合理的。

【讨论】:

【解决方案3】:

这里有一篇关于异常的好文章。

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

结论是堆栈跟踪构造和堆栈展开是昂贵的部分。下面的代码利用了1.7 中的一个特性,我们可以在其中打开和关闭堆栈跟踪。然后我们可以使用它来查看不同场景的成本类型

以下是单独创建对象的时间。我在此处添加了String,因此您可以看到,如果不编写堆栈,则创建JavaException 对象和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)

下面显示了从特定深度投掷一百万次所需的时间。

|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 (%)| 

以下内容几乎可以肯定是过于简单化了...

如果我们在堆栈写入时采用 16 的深度,那么对象创建大约需要大约 40% 的时间,实际堆栈跟踪占了绝大多数。约 93% 的 JavaException 对象实例化是由于堆栈跟踪。这意味着在这种情况下展开堆栈需要另外 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];
  };
}

与您通常发现的相比,此示例中的堆栈帧很小。

你可以使用 javap 查看字节码

javap -c -v -constants JavaException.class

即这是用于方法 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

【讨论】:

    【解决方案4】:

    使用null 堆栈跟踪创建Exception 所花费的时间与throwtry-catch 块一起花费的时间差不多。但是,填充堆栈跟踪平均需要 5 倍的时间

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

    class NoStackException extends Exception{
        public NoStackException() {
            super("",null,false,false);
        }
    }
    

    基准代码如下:

    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;
        }
    
    }
    

    输出:

    throwCatchTime = 19
    newExceptionTime = 77
    newObjectTime = 3
    noStackExceptionTime = 15
    

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

    【讨论】:

    • 能否再添加一种情况,在开始时间之前创建一个异常实例,然后在循环中重复抛出 + 捕获它?这将显示仅投掷 + 接球的成本。
    • @MartinCarney 好建议!我更新了我的答案来做到这一点。
    • 我对您的测试代码做了一些调整,看起来编译器正在做一些优化,这会阻止我们获得准确的数字。
    • @MartinCarney 我更新了折扣编译器优化的答案
    • 仅供参考,您可能应该阅读How do I write a correct micro-benchmark in Java? 的答案 提示:不是这样。
    【解决方案5】:

    这部分问题...

    问这个问题的另一种方法是,如果我创建了一个 Exception 实例并且 一遍又一遍地扔和抓住它,那会更快吗 而不是每次抛出时都创建一个新的异常?

    似乎是在询问是否创建异常并将其缓存在某处可以提高性能。是的,它确实。这与在创建对象时关闭正在写入的堆栈相同,因为它已经完成了。

    这些是我得到的时间,请在此之后阅读警告......

    |Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)|
    |   16|            193|             251| 77 (%)| 
    |   15|            390|             406| 96 (%)| 
    |   14|            394|             401| 98 (%)| 
    |   13|            381|             385| 99 (%)| 
    |   12|            387|             370| 105 (%)| 
    |   11|            368|             376| 98 (%)| 
    |   10|            188|             192| 98 (%)| 
    |    9|            193|             195| 99 (%)| 
    |    8|            200|             188| 106 (%)| 
    |    7|            187|             184| 102 (%)| 
    |    6|            196|             200| 98 (%)| 
    |    5|            197|             193| 102 (%)| 
    |    4|            198|             190| 104 (%)| 
    |    3|            193|             183| 105 (%)| 
    

    当然,这个问题是你的堆栈跟踪现在指向你实例化对象的位置而不是它被抛出的位置。

    【讨论】:

      【解决方案6】:

      以@AustinD 的回答为出发点,我做了一些调整。代码在底部。

      除了增加一个Exception实例重复抛出的情况外,我还关闭了编译器优化,以便我们得到准确的性能结果。根据this answer,我将-Djava.compiler=NONE 添加到VM 参数中。 (在 eclipse 中,编辑 Run Configuration → Arguments 来设置这个 VM 参数)

      结果:

      new Exception + throw/catch = 643.5
      new Exception only          = 510.7
      throw/catch only            = 115.2
      new String (benchmark)      = 669.8
      

      因此,创建异常的成本大约是抛出 + 捕获它的 5 倍。假设编译器没有优化掉大部分成本。

      为了比较,下面是没有禁用优化的相同测试运行:

      new Exception + throw/catch = 382.6
      new Exception only          = 379.5
      throw/catch only            = 0.3
      new String (benchmark)      = 15.6
      

      代码:

      public class ExceptionPerformanceTest {
      
          private static final int NUM_TRIES = 1000000;
      
          public static void main(String[] args) {
      
              double numIterations = 10;
      
              long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0;
      
              for (int i = 0; i < numIterations; i++) {
                  exceptionPlusCatchTime += exceptionPlusCatchBlock();
                  excepTime += createException();
                  throwTime += catchBlock();
                  strTime += createString();
              }
      
              System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations);
              System.out.println("new Exception only          = " + excepTime / numIterations);
              System.out.println("throw/catch only            = " + throwTime / numIterations);
              System.out.println("new String (benchmark)      = " + strTime / numIterations);
      
          }
      
          private static long exceptionPlusCatchBlock() {
              long start = System.currentTimeMillis();
              for (int i = 0; i < NUM_TRIES; i++) {
                  try {
                      throw new Exception();
                  } catch (Exception e) {
                      // do nothing
                  }
              }
              long stop = System.currentTimeMillis();
              return stop - start;
          }
      
          private static long createException() {
              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 createString() {
              long start = System.currentTimeMillis();
              for (int i = 0; i < NUM_TRIES; i++) {
                  Object o = new String("" + i);
              }
              long stop = System.currentTimeMillis();
              return stop - start;
          }
      
          private static long catchBlock() {
              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;
          }
      }
      

      【讨论】:

      • 禁用优化 = 很棒的技术!我将编辑我的原始答案,以免误导任何人
      • 禁用优化并不比编写有缺陷的基准测试更好,因为纯解释模式与实际性能无关。 JVM 的强大之处在于 JIT 编译器,那么衡量一些不能反映实际应用程序如何工作的东西有什么意义呢?
      • 创建、抛出和捕获异常的方面比这个“基准”中所讨论的要多得多。我强烈建议您阅读this post
      猜你喜欢
      • 2013-05-03
      • 2010-10-12
      • 1970-01-01
      • 2010-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-15
      相关资源
      最近更新 更多