Какая часть создания исключения является дорогой?

256

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

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

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

Exception e = new Exception();

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

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

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

Мартин Карни
источник
20
Я считаю, что он заполняет и заполняет трассировку стека.
Эллиот Фриш
12
Проверьте это: stackoverflow.com/questions/16451777/…
Хорхе
«если я сделал один экземпляр Exception и бросил и поймал его снова и снова», когда создается исключение, его стековая трасса заполняется, что означает, что оно всегда будет одной и той же stracrace независимо от места, из которого оно было выброшено. Если трассировка стека не важна для вас, вы можете попробовать свою идею, но это может сделать отладку очень сложной, если не невозможной в некоторых случаях.
Пшемо
2
@Pshemo Я не планирую на самом деле сделать это в коде, я спрашиваю о производительности, и используя этот абсурд в качестве примера , в котором он мог бы изменить ситуацию.
Мартин Карни
@MartinCarney Я добавил ответ в ваш последний абзац, то есть кэширование исключения приведет к увеличению производительности. Если это полезно, я могу добавить код, если нет, я могу удалить ответ.
Гарри

Ответы:

267

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

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


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

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

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


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

apangin
источник
8
Как отмечено в статье и затронуто здесь, в результате стоимость выбрасывания / перехвата исключений сильно зависит от глубины вызовов. Дело в том, что утверждение «исключения являются дорогими» не совсем правильно. Более правильное утверждение состоит в том, что исключения могут быть дорогими. Честно говоря, я думаю, что высказывание об использовании исключений только для «действительно исключительных случаев» (как в статье) слишком сильно сформулировано. Они идеально подходят практически для всего, что находится за пределами нормального обратного потока, и трудно определить влияние на производительность их использования в реальном приложении.
JimmyJames
14
Может быть, стоит количественно оценить накладные расходы на исключения. Даже в самом худшем случае, о котором сообщается в этой довольно исчерпывающей статье (создание и отлов динамического исключения с трассировкой стека, которая фактически запрашивается, глубиной 1000 кадров стека), занимает 80 микросекунд. Это может быть важно, если вашей системе требуется обрабатывать тысячи исключений в секунду, но в остальном беспокоиться не стоит. И это худший случай; если ваши стековые трассировки немного более разумны или вы не запрашиваете их, мы можем обработать почти миллион исключений в секунду.
меритон
13
Я подчеркиваю это, потому что многие люди, прочитав, что исключения являются «дорогими», никогда не перестанут спрашивать «дорого по сравнению с чем», но предполагают, что они являются «дорогой частью их программы», которой они очень редко бывают.
меритон
2
Есть одна часть, которая здесь не упоминается: потенциальные затраты на предотвращение применения оптимизаций. Экстремальным примером может быть JVM, не включающая, чтобы избежать «запутанных» трассировок стека, но я видел (микро) тесты, в которых наличие или отсутствие исключений приводило бы к срыву оптимизаций в C ++ и раньше.
Матье М.
3
@MatthieuM. Исключения и блоки try / catch не препятствуют встраиванию JVM. Для скомпилированных методов трассировки реального стека восстанавливаются из таблицы фреймов виртуального стека, хранящейся в виде метаданных. Я не могу вспомнить JIT-оптимизацию, которая несовместима с try / catch. Структура try / catch сама по себе ничего не добавляет к коду метода, она существует только как таблица исключений, помимо кода.
Апангин
72

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

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

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

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

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

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

Эриксон
источник
3
Ссылка на конструктор: docs.oracle.com/javase/8/docs/api/java/lang/...
25

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

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% времени, фактическая трассировка стека составляет подавляющее большинство этого. ~ 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
Гарри
источник
13

Создание трассировки Exceptionсо nullстеком занимает примерно столько же времени, сколько throwи try-catchблок вместе. Однако заполнение трассировки стека занимает в среднем в 5 раз больше времени .

Я создал следующий тест, чтобы продемонстрировать влияние на производительность. Я добавил в -Djava.compiler=NONERun Configuration, чтобы отключить оптимизацию компилятора. Чтобы измерить влияние построения трассировки стека, я расширил 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примерно так же дорого, как и повторение одного и того же Exception. Это также показывает, что создание Exceptionи заполнение его трассировки стека занимает примерно в 4 раза больше времени.

Остин Д
источник
1
Не могли бы вы добавить еще один случай, когда вы создали один экземпляр Exception до времени начала, а затем бросили + перехватили его несколько раз в цикле? Это показало бы стоимость просто броска + ловли.
Мартин Карни
@MartinCarney Отличное предложение! Я обновил свой ответ, чтобы сделать это.
Остин Д
Я немного изменил ваш тестовый код, и похоже, что компилятор выполняет некоторую оптимизацию, которая мешает нам получать точные цифры.
Мартин Карни
@MartinCarney Я обновил ответ на оптимизацию компилятора скидок
Остин D
К вашему сведению, вы, вероятно, должны прочитать ответы на Как написать правильный микро-тест на Java? Подсказка: это не так.
Даниэль Приден
4

Эта часть вопроса ...

Еще один способ задать вопрос: если я сделал один экземпляр 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 отредактируйте 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;
    }
}
Мартин Карни
источник
Отключение оптимизации = отличная техника! Я отредактирую свой оригинальный ответ, чтобы никого не вводить в заблуждение
Остин Д
3
Отключение оптимизации не лучше, чем написание некорректного теста, поскольку чистый интерпретируемый режим не имеет ничего общего с реальной производительностью. Мощь JVM заключается в JIT-компиляторе, так какой смысл измерять то, что не отражает работу реального приложения?
Апангин
2
Существует намного больше аспектов создания, создания и отлова исключений, чем указано в этом «эталоне». Я настоятельно рекомендую вам прочитать этот пост .
Апангин