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


256

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

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

换句话说,如果我这样做

Exception e = new Exception();

但是不要扔,是扔的大部分费用,还是扔+渔获物处理的成本很高?

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

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


20
我相信它正在填充并填充堆栈跟踪。
艾略特·弗里施


“如果我创建了一个Exception实例并一遍又一遍地捕获它,”当创建异常时,其堆栈跟踪将被填充,这意味着无论从何处抛出异常,它将始终是相同的触角。如果stacktrace对您不重要,那么您可以尝试一下,但这可能会使调试变得非常困难,即使在某些情况下并非不可能。
Pshemo '16

2
@Pshemo我不打算在代码中实际执行此操作,而是询问性能,并以这种荒谬为例说明可能会有所作为。
马丁·卡尼

@MartinCarney我已经在您的最后一段中添加了答案,即缓存异常会提高性能。如果有用,我可以添加代码,否则可以删除答案。
哈利

Answers:


267

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

有关高异常代价的神话来自大多数Throwable构造函数都隐式调用的事实fillInStackTrace。但是,有一个构造函数可以创建Throwable没有堆栈跟踪的。它使您可以快速实例化可抛出对象。创建轻量级异常的另一种方法是重写fillInStackTrace


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

如果使用相同的方法(或更确切地说,在相同的上下文中,因为上下文可以由于内联而包含多种方法)捕获它,那么throwgoto(与JIT编译之后)一样简单快捷。

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


我可以通过适当的基准确认以上陈述,但是幸运的是我不需要这样做,因为HotSpot的性能工程师Alexey Shipilev的帖子已经很好地涵盖了所有方面:Lil'Exception 的出色表现


8
如本文所述并在此处提到,结果是抛出/捕获异常的代价高度依赖于调用的深度。这里的重点是,“异常昂贵”这样的说法并不正确。一个更正确的说法是,异常“可能”代价高昂。坦白地说,我认为仅在“真正的例外情况”(如本文中)中使用例外措辞过于强烈。它们非常适合正常返回流以外的任何事物,并且很难检测到在实际应用中以这种方式使用它们对性能的影响。
JimmyJames

14
量化异常的开销可能是值得的。即使在这篇相当详尽的文章中报告的最坏情况下(抛出并捕获具有实际查询的堆栈跟踪的动态异常,深度为1000堆栈帧),也需要80微秒。如果您的系统需要每秒处理数千个异常,那将是非常重要的,但不值得担心。那是最坏的情况;如果您的堆栈跟踪信息更加精巧,或者您不查询堆栈跟踪信息,那么我们每秒可以处理近一百万个异常。
meriton '16

13
我之所以强调这一点,是因为许多人在读到异常是“昂贵的”后,就不停地问“昂贵而不是什么”,而是假设它们是“程序中的昂贵部分”,而这种情况很少出现。
meriton '16

2
这里没有提到一个部分:防止应用优化的潜在成本。一个极端的例子是JVM不能内联以避免“弄乱”堆栈跟踪,但是我已经看到了(微)基准测试,在该基准测试中,存在或不存在异常都会在C ++中实现或破坏优化。
Matthieu M.16年

3
@MatthieuM。异常和try / catch块不会阻止JVM内联。对于编译方法,实际的堆栈跟踪是从存储为元数据的虚拟堆栈帧表中重建的。我想不起来与try / catch不兼容的JIT优化。try / catch结构本身不向方法代码添加任何内容,它仅作为代码之外的异常表存在。
apangin '16

72

在大多数Throwable构造函数中,第一个操作是填写堆栈跟踪,这是大部分开销所在。

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

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

当前版本的Java进行了一些尝试来优化堆栈跟踪的创建。调用本机代码以填充堆栈跟踪,该跟踪以更轻量的本机结构记录跟踪。StackTraceElement仅当调用,或需要跟踪的其他方法时getStackTrace(),才从此记录懒惰地创建相应的Java 对象printStackTrace()

如果消除了堆栈跟踪的生成,则另一个主要成本是在抛出和捕获之间展开堆栈。在捕获异常之前遇到的介入帧越少,则越快。

设计您的程序,以便仅在真正异常的情况下才引发异常,并且很难证明此类优化。



25

这里有很多关于异常的文章。

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

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

以下是仅创建对象的时间。我在String此处添加了内容,因此您可以看到,无需编写堆栈,创建JavaExceptionObject和a 几乎没有区别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%的时间,实际堆栈跟踪占了绝大多数。实例化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];
  };
}

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

您可以使用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

13

Exception使用null堆栈跟踪创建所需的时间与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

这意味着创建一个a NoStackException与重复抛出该a 差不多昂贵Exception。它还显示创建Exception并填充其堆栈跟踪大约需要4倍的时间。


1
您是否可以再添加一种情况,即在开始时间之前创建一个Exception实例,然后在循环中反复抛出+捕获它?那将显示出仅仅投掷+接球的成本。
马丁·卡尼

@MartinCarney很棒的建议!我更新了答案以做到这一点。
奥斯丁D

我对测试代码进行了一些调整,看起来编译器正在进行一些优化,从而阻止了我们获得准确的数字。
马丁·卡尼

@MartinCarney我更新了折扣编译器优化的答案
Austin D'

仅供参考,您可能应该阅读“ 如何在Java中编写正确的微基准测试”的答案提示:不是。
Daniel Pryden '16

4

这部分问题...

提出此问题的另一种方式是,如果我创建了一个Exception实例并一遍又一遍地抛出并捕获,那么这会比每次抛出新的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 (%)| 

当然,问题在于您的堆栈跟踪现在指向实例化对象的位置,而不是对象从何处抛出。


3

以@AustinD的答案为起点,我进行了一些调整。底部的代码。

除了添加重复抛出一个Exception实例的情况之外,我还关闭了编译器优化,以便我们可以获得准确的性能结果。-Djava.compiler=NONE按照这个答案,我添加了VM参数。(在Eclipse中,编辑运行配置→参数以设置此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;
    }
}

禁用优化=出色的技术!我将编辑原始答案,以免误导任何人
Austin D

3
禁用优化并不比编写有缺陷的基准更好,因为纯解释模式与实际性能无关。JVM的功能是JIT编译器,因此衡量不反映实际应用程序工作方式的东西有什么意义?
apangin '16

2
创建,引发和捕获异常的方面比在此“基准”中融合的要多得多。我强烈建议您阅读这篇文章
apangin '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.