Яка частина кидання винятку дорога?


256

У Java використання "кидання / лову" як частини логіки, коли насправді помилки немає, як правило, погана ідея (частково), тому що кидати та виловлювати виняток дорого, і робити це багато разів у циклі, як правило, набагато повільніше, ніж у інших контрольні структури, які не передбачають кидання виключень.

Моє запитання полягає в тому, чи є витрати, понесені в самому киданні / лові, або при створенні об'єкта Exception (оскільки він отримує багато інформації про час виконання, включаючи стек виконання)?

Іншими словами, якщо я

Exception e = new Exception();

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

Я не запитую, чи додавання коду до блоку спробу / лову додає вартість виконання цього коду, я запитую, чи є виловлення винятку дорогою частиною, або створення (виклик конструктора для) винятку - дорога частина .

Ще один спосіб запитати це, якщо я зробив один екземпляр винятку і кинув його і перехоплював його знову і знову, чи буде це значно швидше, ніж створювати новий виняток щоразу, коли я кидаю?


20
Я вважаю, що це заповнення та заповнення сліду стека.
Елліот Фріш


"якщо я зробив один екземпляр винятку і кинув і спіймав його знову і знову", коли створюється виняток, його стек-трак заповнюється, це означає, що він завжди буде однаковою трактацією, незалежно від місця, з якого він був кинутий. Якщо стек-трек для вас не важливий, ніж ви можете спробувати свою ідею, але це може зробити налагодження дуже важким, якщо не неможливим в деяких випадках.
Пшемо

2
@Pshemo Я не планую насправді робити це в коді, я запитую про продуктивність і використовую цю абсурдність як приклад, коли це може змінити значення.
Мартін Карні

@MartinCarney Я додав відповідь до останнього пункту, тобто кешування винятку матиме підвищення продуктивності. Якщо це корисно, я можу додати код, якщо ні, я можу видалити відповідь.
Гаррі

Відповіді:


267

Створення об’єкта винятку не дорожче, ніж створення інших звичайних об'єктів. Основна вартість прихована рідною мовоюfillInStackTrace методі, який проходить через стек викликів і збирає всю необхідну інформацію для побудови сліду стека: класи, назви методів, номери рядків тощо.

Міф про високі витрати на винятки походить від того, що більшість Throwableконструкторів неявно називають fillInStackTrace. Однак є один конструктор для створення Throwableсліду без стека. Це дозволяє робити метальні деталі, які дуже швидко інстанціюються. Ще один спосіб створити легкі винятки - це перекриття fillInStackTrace.


А як же кинути виняток?
Насправді це залежить від того, куди потрапив викинутий виняток .

Якщо він потрапляє в один і той же метод (або, точніше, в той самий контекст, оскільки контекст може включати в себе кілька методів за рахунок вбудовування), то throwце так само швидко і просто, якgoto (звичайно, після компіляції JIT).

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


Я міг би підтвердити вищезазначені твердження належними орієнтирами, але, на щастя, мені цього не потрібно, оскільки всі аспекти вже чудово висвітлюються на посаді інженера-виконавця HotSpot Олексія Шипільова: Винятковий показник винятку Lil ' .


8
Як зазначається у статті та торкається тут, підсумок полягає в тому, що вартість кидання / вилучення винятків сильно залежить від глибини дзвінків. Суть у тому, що твердження "винятки дорогі" насправді не є правильним. Більш правильне твердження полягає в тому, що винятки "можуть" бути дорогими. Чесно кажучи, я вважаю, що сказати, що використовувати лише винятки для "справді виняткових випадків" (як у статті) занадто сильно сказано. Вони ідеально підходять для майже нічого поза нормальним потоком повернення, і важко виявити вплив продуктивності, використовуючи їх таким чином у реальному застосуванні.
JimmyJames

14
Можливо, варто оцінити кількість винятків. Навіть у найгіршому випадку, про який повідомляється у цій досить вичерпній статті (кидання та вилучення динамічного винятку із стек-треком, який насправді запитується, 1000 кадрів стека в глибину), займає 80 мікросекунд. Це може бути суттєво, якщо вашій системі потрібно обробляти тисячі винятків за секунду, але в іншому випадку не варто турбуватися. І це найгірший випадок; якщо ваші стек-траси трохи безпечніші, або ви не запитуєте їх стек-трак, ми можемо обробити майже мільйон винятків за секунду.
meriton

13
Я наголошую на цьому, тому що багато людей, читаючи, що винятки є "дорогими", ніколи не зупиняються, щоб запитувати "дорого порівняно з тим", але припускають, що вони "дорога частина їх програми", що вони дуже рідко є.
meriton

2
Є одна частина, яка тут не згадується: потенційна вартість у запобіганні застосуванню оптимізацій. Надзвичайним прикладом може слугувати JVM, що не дає змоги уникнути «заплутаних» слідів стеку, але я бачив (мікро) орієнтири, де наявність чи відсутність винятків зробить або порушить оптимізацію в C ++ раніше.
Маттьє М.

3
@MatthieuM. Винятки та блоки "try / catch" не заважають JVM вбудовуватися. Для компільованих методів реальні сліди стека реконструюються з таблиці віртуального кадру стека, що зберігається як метадані. Я не можу згадати оптимізацію JIT, несумісну з спробою / ловити. Сама структура try / catch нічого не додає до методу коду, вона існує лише як таблиця винятків, окрім коду.
apangin

72

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

Однак існує захищений конструктор з прапором, щоб відключити трасування стека. Цей конструктор доступний під час розширенняException . Якщо ви створюєте спеціальний тип виключення, ви можете уникнути створення сліду стека та отримати кращу ефективність за рахунок меншої кількості інформації.

Якщо ви створюєте єдиний виняток будь-якого типу звичайними засобами, ви можете повторно кинути його без накладного заповнення сліду стека. Однак його стек слід відображати там, де він був побудований, а не там, де він був кинутий у конкретному екземплярі.

Поточні версії Java роблять кілька спроб оптимізувати створення слідів стека. Рідний код викликається для заповнення сліду стека, який записує слід у більш легкій, рідній структурі. Відповідні Java StackTraceElementоб'єкти ліниво створені з цього запису тільки тоді , коли getStackTrace(),printStackTrace() або інші методи , які наказують слід називаються.

Якщо ви усунете генерацію сліду стека, іншою основною вартістю є розмотування стека між кидком та уловом. Чим менше кадрів, що втручаються, перш ніж вилучення винятку, тим швидше це буде.

Створіть програму так, щоб винятки кидались лише у справді виняткових випадках, а такі оптимізації важко обґрунтувати.


3
Посилання на конструктор: docs.oracle.com/javase/8/docs/api/java/lang/…

25

Тут добре запишіться на Винятки.

http://shipilev.net/blog/2014/exceptions-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=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 рази більше.


1
Чи можете ви додати ще один випадок, коли ви створюєте один екземпляр винятку перед початком часу, а потім кидаєте + виловлюєте його кілька разів у циклі? Це показало б вартість просто кидання + лову.
Мартін Керні

@MartinCarney Чудова пропозиція! Я оновив свою відповідь, щоб зробити саме це.
Остін Д

Я трохи змінив ваш тестовий код, і, схоже, компілятор робить певну оптимізацію, яка заважає нам отримувати точні цифри.
Мартін Керні

@MartinCarney Я оновив відповідь на оптимізацію компілятора знижок
Остін Д

FYI, напевно, ви повинні прочитати відповіді на тему: Як написати правильний мікро-орієнтир на Java? Підказка: це не все.
Даніель Приден

4

Ця частина питання ...

Ще один спосіб запитати це, якщо я зробив один екземпляр винятку і кинув його і перехоплював його знову і знову, чи буде це значно швидше, ніж створювати новий виняток щоразу, коли я кидаю?

Здається, запитує, чи створення винятку та кешування його десь покращує ефективність. Так. Це те саме, що вимкнення стека, записаного при створенні об'єкта, тому що це вже зроблено.

Це моменти, які я отримав, будь ласка, прочитайте попередження після цього ...

|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 як вихідну точку, я зробив декілька налаштувань. Код внизу.

На додаток до додання випадку, коли один екземпляр винятку викидається неодноразово, я також відключив оптимізацію компілятора, щоб ми могли отримати точні результати роботи. Я додав -Djava.compiler=NONEдо аргументів VM, відповідно до цієї відповіді . (У затемненні відредагуйте Конфігурація запуску → Аргументи, щоб встановити цей аргумент 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, тож який сенс вимірювати щось, що не відображає, як працює реальна програма?
apangin

2
Існує набагато більше аспектів створення, викидання та вилучення винятків, ніж описано в цьому «еталоні». Я настійно пропоную вам прочитати цю публікацію .
apangin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.