Какая часть бросания Исключения стоит дорого?

В Java, используя throw/catch как часть логики, когда на самом деле ошибка вообще не является плохой идеей (частично), потому что бросать и ловить исключение дорого, а делать это много раз в цикле, как правило, далеко медленнее, чем другие структуры управления, которые не включают в себя исключения для исключения.

Мой вопрос в том, связана ли стоимость с самим броском /catch или при создании объекта Exception (поскольку он получает много информации о времени выполнения, включая стек выполнения)?

Другими словами, если я делаю

Exception e = new Exception();

но не бросайте его, это то, что большая часть стоимости бросания, или бросок + поймать обработку, что дорого?

Я не спрашиваю, добавляет ли код в блок try/catch стоимость выполнения этого кода, я спрашиваю, является ли перехват Exception дорогостоящей частью или создание (вызов конструктора для) Exception дорогая часть.

Еще один способ задать вопрос: если я сделал один экземпляр Exception и бросил его и поймал его снова и снова, это было бы значительно быстрее, чем создание нового исключения каждый раз, когда я бросаю?

Ответ 1

Создание объекта исключения не дороже, чем создание других обычных объектов. Основная стоимость скрыта в собственном методе fillInStackTrace который проходит по стеку вызовов и собирает всю необходимую информацию для построения трассировки стека: классы, имена методов, номера строк и т.д.

Миф о высокой стоимости исключений проистекает из того факта, что большинство конструкторов Throwable неявно вызывают fillInStackTrace. Однако есть один конструктор для создания Throwable без трассировки стека. Это позволяет создавать броски, которые очень быстро создаются. Другой способ создать легкие исключения - переопределить fillInStackTrace.


Теперь насчет бросать исключение?
На самом деле, это зависит от того, где поймали выброшенное исключение.

Если он перехватывается одним и тем же методом (или, точнее, в одном и том же контексте, поскольку контекст может включать несколько методов из-за встраивания), то throw будет таким же быстрым и простым, как goto (конечно, после JIT-компиляции).

Однако если блок catch находится где-то глубже в стеке, JVM необходимо размотать кадры стека, а это может занять значительно больше времени. Это занимает еще больше времени, если задействованы synchronized блоки или методы, потому что разматывание подразумевает освобождение мониторов, принадлежащих удаленным кадрам стека.


Я мог бы подтвердить вышеприведенные утверждения с помощью надлежащих тестов, но, к счастью, мне не нужно этого делать, поскольку все аспекты уже отлично освещены в посте инженера по производительности HotSpot Алексея Шипилева: Исключительная производительность от Lil 'Exception.

Ответ 2

Первая операция в большинстве конструкторов Throwable заключается в заполнении трассировки стека, в которой находится большая часть расходов.

Существует, однако, защищенный конструктор с флагом для отключения трассировки стека. Этот конструктор доступен при расширении Exception. Если вы создадите собственный тип исключения, вы можете избежать создания трассировки стека и получить лучшую производительность за счет меньшего количества информации.

Если вы создаете единственное исключение любого типа обычными средствами, вы можете повторно его перебрасывать много раз без накладных расходов на заполнение трассировки стека. Тем не менее, его трассировка стека будет отражать, где она была построена, а не там, где она была выбрана в конкретном экземпляре.

Текущие версии Java делают некоторые попытки оптимизировать создание трассировки стека. Для заполнения трассировки стека вызывается собственный код, который записывает трассировку в более легкой, родной структуре. Соответствующие объекты Java StackTraceElement лениво создаются из этой записи только тогда, когда вызываются getStackTrace(), printStackTrace() или другие методы, требующие трассировки.

Если вы исключаете генерацию трассировки стека, другая основная стоимость - это разматывание стека между броском и уловкой. Чем меньше промежуточных кадров, встречающихся перед исключением, поймано, тем быстрее это будет.

Создайте свою программу, чтобы исключения были выброшены только в действительно исключительных случаях, и оптимизация, подобная этим, трудно оправдать.

Ответ 3

Theres хорошая запись об исключениях здесь.

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

Создание трассировки Exception с трассировкой стека null занимает примерно столько же времени, сколько блок throw и try-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 и заполнение его трассировки стека занимает приблизительно 4x дольше.

Ответ 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 вызывается повторно, я также отключил оптимизацию компилятора, чтобы мы могли получить точные результаты производительности. Я добавил -Djava.compiler=NONE к аргументам VM, согласно этому ответу. (В eclipse отредактируйте параметр "Завершить конфигурацию и аргумент", чтобы установить этот аргумент виртуальной машины)

Результаты:

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