Насколько медленны исключения Java?

Вопрос: действительно ли обработка исключений в Java медленная?

Традиционные знания, а также многие результаты Google говорят, что исключительная логика не должна использоваться для нормального выполнения программ на Java. Обычно приводятся две причины:

  1. это действительно медленно - даже на порядок медленнее, чем обычный код (причины могут быть разными),

а также

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

Этот вопрос о № 1.

В качестве примера, эта страница описывает обработку исключений Java как "очень медленную" и связывает медлительность с созданием строки сообщения об исключении - "эта строка затем используется при создании объекта исключения, который генерируется. Это не быстро". В статье " Эффективная обработка исключений в Java" говорится, что "причина этого заключается в том, что создание объекта обрабатывает исключение, что делает создание исключений по своей сути медленным". Другая причина в том, что генерация трассировки стека замедляет его.

Мое тестирование (с использованием Java 1.6.0_07, Java HotSpot 10.0, в 32-битной Linux) показало, что обработка исключений не медленнее, чем в обычном коде. Я попытался запустить метод в цикле, который выполняет некоторый код. В конце метода я использую логическое значение, чтобы указать, возвращать или выбрасывать. Таким образом, фактическая обработка одинакова. Я пытался запускать методы в разных порядках и усреднять время тестирования, думая, что это могло быть разогревом JVM. Во всех моих тестах бросок был, по крайней мере, так же быстро, как возврат, если не быстрее (до 3,1% быстрее). Я полностью открыт к возможности того, что мои тесты были неправильными, но я не видел ничего такого, как пример кода, сравнение тестов или результаты за последние год или два, которые показывают, что обработка исключений в Java действительно медленный.

На этом пути меня привел API, который мне был нужен, чтобы использовать исключения как часть обычной логики управления. Я хотел исправить их в их использовании, но теперь я не смогу. Должен ли я вместо этого похвалить их за их дальновидность?

В статье " Эффективная обработка исключений Java в процессе компиляции точно в срок" авторы предполагают, что одного только наличия обработчиков исключений, даже если не выдается никаких исключений, достаточно для того, чтобы компилятор JIT не оптимизировал код должным образом, тем самым замедляя его, Я еще не проверял эту теорию.

19 ответов

Решение

Это зависит от того, как реализованы исключения. Самый простой способ - использовать setjmp и longjmp. Это означает, что все регистры ЦПУ записываются в стек (что уже занимает некоторое время) и, возможно, необходимо создать некоторые другие данные... все это уже происходит в операторе try. Оператор throw должен разматывать стек и восстанавливать значения всех регистров (и, возможно, других значений в ВМ). Так что try и throw одинаково медленны, и это довольно медленно, однако, если исключение не выдается, выход из блока try в большинстве случаев вообще не занимает времени (поскольку все помещается в стек, который очищается автоматически, если метод существует).

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

... но вы пишете на Java, чтобы ваш код позже работал только на одной JVM в одной конкретной системе? Так как, если он когда-либо будет работать на любой другой платформе или любой другой версии JVM (возможно, любого другого поставщика), кто сказал, что они также используют быструю реализацию? Быстрый более сложный, чем медленный, и его нелегко реализовать во всех системах. Вы хотите остаться портативным? Тогда не надейтесь на быстрые исключения.

Это также имеет большое значение для того, что вы делаете в блоке try. Если вы открываете блок try и никогда не вызываете какой-либо метод из этого блока try, блок try будет очень быстрым, поскольку JIT может фактически обработать бросок как простое goto. Ему не нужно ни сохранять состояние стека, ни разматывать стек, если выбрасывается исключение (ему нужно только перейти к обработчикам перехвата). Тем не менее, это не то, что вы обычно делаете. Обычно вы открываете блок try и затем вызываете метод, который может вызвать исключение, верно? И даже если вы просто используете блок try в своем методе, какой это будет метод, который не вызывает никакой другой метод? Будет ли он просто рассчитать число? Тогда зачем вам исключения? Есть гораздо более элегантные способы регулирования потока программ. Практически для всего остального, кроме простой математики, вам придется вызывать внешний метод, и это уже разрушает преимущество локального блока try.

Смотрите следующий тестовый код:

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

Результат:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

Замедление в блоке try слишком мало, чтобы исключить мешающие факторы, такие как фоновые процессы. Но блок захвата убил все и сделал это в 66 раз медленнее!

Как я уже сказал, результат не будет таким плохим, если вы поместите try/catch и throw все в один и тот же метод (method3), но это специальная оптимизация JIT, на которую я бы не рассчитывал. И даже при использовании этой оптимизации бросок все еще довольно медленный. Так что я не знаю, что вы пытаетесь сделать здесь, но определенно есть лучший способ сделать это, чем использовать try/catch/throw.

К вашему сведению, я продлил эксперимент, который провел Меки:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

Первые 3 такие же, как у Меки (мой ноутбук явно медленнее).

method4 идентичен method3 за исключением того, что он создает new Integer(1) вместо того, чтобы делать throw new Exception(),

method5 похож на method3 за исключением того, что он создает new Exception() не бросая это.

method6 похож на method3 за исключением того, что он генерирует предварительно созданное исключение (переменную экземпляра), а не создает новое.

В Java большая часть затрат на создание исключения - это время, затрачиваемое на сбор трассировки стека, которое происходит при создании объекта исключения. Фактическая стоимость создания исключения, хотя и большая, значительно меньше стоимости создания исключения.

Алексей Шипилёв провел очень тщательный анализ, в котором он сравнил исключения Java при различных сочетаниях условий:

  • Недавно созданные исключения против предварительно созданных исключений
  • Трассировка стека включена против отключена
  • Запрошенная трассировка стека против никогда не запрашиваемой
  • Пойман на верхнем уровне против переброшенного на каждом уровне против закованного / завернутого на каждом уровне
  • Различные уровни глубины стека вызовов Java
  • Нет встроенных оптимизаций по сравнению с экстремальными встроенными по сравнению с настройками по умолчанию
  • Пользовательские поля читаются, а не читаются

Он также сравнивает их с производительностью проверки кода ошибки на разных уровнях частоты ошибок.

Выводы (цитаты дословно из его поста) были:

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

  2. Затраты на производительность для исключений имеют два основных компонента: построение трассировки стека при создании экземпляра Exception и разматывание стека во время генерирования исключения.

  3. Затраты на построение трассировки стека пропорциональны глубине стека в момент создания исключения. Это уже плохо, потому что кто на Земле знает глубину стека, на котором этот метод броска будет вызван? Даже если вы отключите генерацию трассировки стека и / или кэшируете исключения, вы сможете избавиться только от этой части затрат на производительность.

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

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

  6. Оптимистическое эмпирическое правило, кажется, 10^-4 частоты для исключений достаточно исключительным. Это, конечно, зависит от тяжести самих исключений, точных действий, выполняемых в обработчиках исключений и т. Д.

В результате, когда исключение не выдается, вы не платите за него, поэтому, когда исключительное условие достаточно редкое, обработка исключения выполняется быстрее, чем при использовании исключения. if каждый раз. Полный пост очень стоит прочитать.

Мой ответ, к сожалению, слишком длинный, чтобы оставлять сообщения здесь. Итак, позвольте мне кратко изложить здесь и направить вас к http://www.fuwjax.com/how-slow-are-java-exceptions/ для получения подробностей.

Настоящий вопрос здесь заключается не в том, "насколько медленно" сбои регистрируются как исключения "по сравнению с" кодом, который никогда не дает сбоев "?" поскольку принятый ответ мог бы заставить вас поверить. Вместо этого следует задать вопрос: "Насколько медленными являются" ошибки, сообщаемые как исключения ", по сравнению с сообщениями о сбоях, сообщаемыми другими способами?" Как правило, два других способа сообщения о сбоях - либо с помощью значений дозорных, либо с помощью обёрток результатов.

Значения Sentinel - это попытка вернуть один класс в случае успеха и другой в случае неудачи. Вы можете думать об этом, как о возвращении исключения вместо того, чтобы его выбросить. Для этого требуется общий родительский класс с объектом успеха, а затем проверка "instanceof" и пара приведений для получения информации об успехе или ошибке.

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

Обертки Result, с другой стороны, вообще не жертвуют безопасностью типов. Они объединяют информацию об успехах и неудачах в одном классе. Таким образом, вместо "instanceof" они предоставляют "isSuccess()" и геттеры для объектов успеха и ошибок. Однако результирующие объекты примерно в 2 раза медленнее, чем с использованием исключений. Оказывается, что каждый раз создавать новый объект-обертку намного дороже, чем иногда создавать исключение.

Кроме того, исключения - это язык, предоставляющий способ указания на сбой метода. Нет другого способа узнать только из API, какие методы должны работать (в основном) всегда, а какие будут сообщать об ошибке.

Исключения безопаснее, чем часовые, быстрее, чем объекты результата, и менее удивительны, чем оба. Я не предполагаю, что try / catch заменяет if / else, но исключения являются правильным способом сообщения о сбое, даже в бизнес-логике.

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

Я расширил ответы, данные @Mecki и @incarnate, без заполнения трассировки стека для Java.

С Java 7+ мы можем использовать Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace), Но для Java6, смотрите мой ответ на этот вопрос

// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceRuntimeException();
    }
}

public static void main(String[] args) {
    int i;
    long l;
    Test t = new Test();

    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method5(i);
        } catch (RuntimeException e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}

Выход с Java 1.6.0_45, на Core i7, 8 ГБ ОЗУ:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

Таким образом, методы, возвращающие значения, работают быстрее, чем методы, генерирующие исключения. ИМХО, мы не можем спроектировать понятный API, просто используя типы возвращаемых данных как для потоков успеха, так и для ошибок Методы, которые генерируют исключения без отслеживания стека, в 4-5 раз быстрее, чем обычные исключения.

Изменить: NoStackTraceThrowable.java Спасибо@Greg

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}

Некоторое время назад я написал класс для проверки относительной производительности преобразования строк в целые, используя два подхода: (1) вызвать Integer.parseInt() и перехватить исключение, или (2) сопоставить строку с регулярным выражением и вызвать parseInt () только в случае успеха Я использовал регулярное выражение наиболее эффективным способом (т. Е. Создавал объекты Pattern и Matcher перед вмешательством в цикл), и я не печатал и не сохранял трассировки стека из исключений.

Для списка из десяти тысяч строк, если бы все они были действительными числами, подход parseInt () был в четыре раза быстрее, чем подход регулярных выражений. Но если бы только 80% строк были действительными, регулярное выражение было в два раза быстрее parseInt(). И если 20% были действительными, то есть исключение было сгенерировано и перехватывало 80% времени, регулярное выражение было примерно в двадцать раз быстрее, чем parseInt().

Я был удивлен результатом, учитывая, что подход регулярных выражений обрабатывает допустимые строки дважды: один раз для соответствия и снова для parseInt(). Но бросать и ловить исключения более, чем восполнить это. Такая ситуация вряд ли случится очень часто в реальном мире, но если это произойдет, вам определенно не следует использовать технику ловли исключений. Но если вы проверяете только пользовательский ввод или что-то в этом роде, непременно используйте подход parseInt().

Я думаю, что в первой статье упоминается об обход стека вызовов и создании трассировки стека как о дорогой части, и хотя во второй статье это не сказано, я думаю, что это самая дорогая часть создания объекта. У Джона Роуза есть статья, в которой он описывает различные методы ускорения исключений. (Предварительное выделение и повторное использование исключения, исключений без трассировки стека и т. Д.)

Но все же - я думаю, что это следует рассматривать только как необходимое зло, в крайнем случае. Джон делает это для того, чтобы эмулировать функции на других языках, которые (пока) не доступны в JVM. Вы не должны привыкать использовать исключения для потока управления. Особенно не по причинам производительности! Как вы сами упомянули в #2, вы рискуете замаскировать серьезные ошибки в своем коде, и это будет труднее поддерживать для новых программистов.

Мне сказали, что микробенчмарки в Java на удивление трудно понять, особенно когда вы попадаете на территорию JIT, поэтому я действительно сомневаюсь, что использование исключений быстрее, чем "возврат" в реальной жизни. Например, я подозреваю, что в вашем тесте где-то между 2 и 5 стековыми фреймами? Теперь представьте, что ваш код будет вызываться компонентом JSF, развернутым JBoss. Теперь у вас может быть трассировка стека длиной в несколько страниц.

Возможно, вы могли бы опубликовать свой тестовый код?

Не знаю, относятся ли эти темы, но я однажды хотел реализовать один трюк, основанный на трассировке стека текущего потока: я хотел узнать имя метода, который вызвал создание экземпляра внутри экземпляра класса (да, идея сумасшедшая, Я полностью бросил это). Итак, я обнаружил, что призвание Thread.currentThread().getStackTrace() очень медленно (из-за родного dumpThreads метод, который он использует внутри).

Итак, Ява Throwableсоответственно имеет нативный метод fillInStackTrace, Я думаю, что убийцаcatch Блок, описанный ранее, как-то запускает выполнение этого метода.

Но позвольте мне рассказать вам другую историю...

В Scala некоторые функциональные возможности компилируются в JVM с использованием ControlThrowable, который расширяет Throwable и переопределяет его fillInStackTrace следующим образом:

override def fillInStackTrace(): Throwable = this

Поэтому я адаптировал тест выше (количество циклов уменьшилось на десять, моя машина немного медленнее:):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

Итак, результаты:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

Вы видите, единственная разница между method3 а также method4 является то, что они бросают различные виды исключений. Yeap, method4 все еще медленнее, чем method1 а также method2, но разница гораздо более приемлема.

Я провел некоторое тестирование производительности с JVM 1.5, и использование исключений было как минимум в 2 раза медленнее. В среднем: время выполнения тривиально небольшого метода более чем в три раза (3 раза) с исключениями. Тривиально небольшая петля, которая должна была поймать исключение, увеличила время в 2 раза.

Я видел похожие цифры в производственном коде, а также в микро-тестах.

Исключения не должны определенно использоваться для всего, что часто вызывается. Бросок тысяч исключений в секунду вызовет огромную горлышко бутылки.

Например, использование "Integer.ParseInt(...)" для поиска всех неверных значений в очень большом текстовом файле - очень плохая идея. (Я видел, как этот служебный метод убивает производительность в рабочем коде)

Использование исключения для сообщения о плохом значении в форме пользовательского графического интерфейса, вероятно, не так уж плохо с точки зрения производительности.

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

Исключительная производительность в Java и C# оставляет желать лучшего.

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

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

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

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

Вот пример кода, который сравнивает производительность исключения с производительностью возвращаемого значения ошибки.

открытый класс TestIt {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

public static void main(String[] args) {
    int i;
    long l;
    TestIt t = new TestIt();
    int failures;

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

}

И вот результаты:

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

Проверка и распространение возвращаемых значений действительно добавляет некоторую стоимость к вызову baseline-null, и эта стоимость пропорциональна глубине вызова. При глубине цепочки вызовов, равной 8, версия проверки возвращаемого значения ошибки была примерно на 27% медленнее, чем базовая версия, которая не проверяла возвращаемые значения.

В отличие от этого, производительность исключений зависит не от глубины вызова, а от частоты исключений. Однако деградация при увеличении частоты исключений гораздо более драматична. Только с частотой ошибок 25% код работал в 24 раза медленнее. При частоте ошибок 100% версия исключения почти в 100 раз медленнее.

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

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

Я думаю, что это на самом деле не отвечает на вопрос, хотя. Я предполагаю, что "общепринятая" мудрость медленного создания исключений была верна в более ранних версиях Java (< 1.4). Создание исключения требует, чтобы виртуальная машина создала всю трассировку стека. С тех пор многое изменилось в ВМ, чтобы ускорить процесс, и это, вероятно, одна область, которая была улучшена.

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

Отличный пост о выполнении исключений:

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

Инстанцирование против повторного использования существующих, с трассировкой стека и без, и т. Д.:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

В зависимости от глубины трассировки стека:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

Для других деталей (включая ассемблер x64 от JIT) прочитайте оригинальное сообщение в блоге.

Это означает, что Hibernate/Spring/etc-EE-shit работают медленно из-за исключений (xD) и перезаписывают поток управления приложения в сторону от исключений (замените его на continure / break и возвращаясь boolean флаги как в C из вызова метода) улучшают производительность вашего приложения в 10-100 раз, в зависимости от того, как часто вы их бросаете))

Просто сравните, скажем, Integer.parseInt со следующим методом, который просто возвращает значение по умолчанию в случае непарсируемых данных, а не генерирует исключение:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

Пока вы применяете оба метода к "действительным" данным, они будут работать примерно с одинаковой скоростью (даже если Integer.parseInt обрабатывает более сложные данные). Но как только вы попытаетесь проанализировать недействительные данные (например, проанализировать "abc" 1 000 000 раз), разница в производительности должна быть существенной.

Используя прилагаемый код на JDK 15, я получаю совершенно разные результаты для тестового примера @Mecki. Это в основном запускает код в 5 циклах, причем первый цикл немного короче, чтобы дать виртуальной машине время для разогрева.

Результаты, достижения:

      Loop 1 10000 cycles
method1 took 1 ms, result was 2
method2 took 0 ms, result was 2
method3 took 22 ms, result was 2
method4 took 22 ms, result was 2
method5 took 24 ms, result was 2
Loop 2 10000000 cycles
method1 took 39 ms, result was 2
method2 took 39 ms, result was 2
method3 took 1558 ms, result was 2
method4 took 1640 ms, result was 2
method5 took 1717 ms, result was 2
Loop 3 10000000 cycles
method1 took 49 ms, result was 2
method2 took 48 ms, result was 2
method3 took 126 ms, result was 2
method4 took 88 ms, result was 2
method5 took 87 ms, result was 2
Loop 4 10000000 cycles
method1 took 34 ms, result was 2
method2 took 34 ms, result was 2
method3 took 33 ms, result was 2
method4 took 98 ms, result was 2
method5 took 58 ms, result was 2
Loop 5 10000000 cycles
method1 took 34 ms, result was 2
method2 took 33 ms, result was 2
method3 took 33 ms, result was 2
method4 took 48 ms, result was 2
method5 took 49 ms, result was 2
      package hs.jfx.eventstream.api;

public class Snippet {
  int value;


  public int getValue() {
      return value;
  }

  public void reset() {
      value = 0;
  }

  // Calculates without exception
  public void method1(int i) {
      value = ((value + i) / i) << 1;
      // Will never be true
      if ((i & 0xFFFFFFF) == 1000000000) {
          System.out.println("You'll never see this!");
      }
  }

  // Could in theory throw one, but never will
  public void method2(int i) throws Exception {
      value = ((value + i) / i) << 1;
      // Will never be true
      if ((i & 0xFFFFFFF) == 1000000000) {
          throw new Exception();
      }
  }

  private static final NoStackTraceRuntimeException E = new NoStackTraceRuntimeException();

  // This one will regularly throw one
  public void method3(int i) throws NoStackTraceRuntimeException {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw E;
      }
  }

  // This one will regularly throw one
  public void method4(int i) throws NoStackTraceThrowable {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw new NoStackTraceThrowable();
      }
  }

  // This one will regularly throw one
  public void method5(int i) throws NoStackTraceRuntimeException {
      value = ((value + i) / i) << 1;
      // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
      // an AND operation between two integers. The size of the number plays
      // no role. AND on 32 BIT always ANDs all 32 bits
      if ((i & 0x1) == 1) {
          throw new NoStackTraceRuntimeException();
      }
  }

  public static void main(String[] args) {
    for(int k = 0; k < 5; k++) {
      int cycles = 10000000;
      if(k == 0) {
        cycles = 10000;
        try {
          Thread.sleep(500);
        }
        catch(InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
      }
      System.out.println("Loop " + (k + 1) + " " + cycles + " cycles");
      int i;
      long l;
      Snippet t = new Snippet();

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          t.method1(i);
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method1 took " + l + " ms, result was " + t.getValue()
      );

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method2(i);
          } catch (Exception e) {
              System.out.println("You'll never see this!");
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method2 took " + l + " ms, result was " + t.getValue()
      );

      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method3(i);
          } catch (NoStackTraceRuntimeException e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println(
          "method3 took " + l + " ms, result was " + t.getValue()
      );


      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method4(i);
          } catch (NoStackTraceThrowable e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


      l = System.currentTimeMillis();
      t.reset();
      for (i = 1; i < cycles; i++) {
          try {
              t.method5(i);
          } catch (RuntimeException e) {
            // always comes here
          }
      }
      l = System.currentTimeMillis() - l;
      System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
    }
  }

  public static class NoStackTraceRuntimeException extends RuntimeException {
    public NoStackTraceRuntimeException() {
        super("my special throwable", null, false, false);
    }
  }

  public static class NoStackTraceThrowable extends Throwable {
    public NoStackTraceThrowable() {
        super("my special throwable", null, false, false);
    }
  }
}

Я изменил ответ @Mecki выше, чтобы method1 возвращал логическое значение и проверку в вызывающем методе, поскольку вы не можете просто заменить Exception ничем. После двух запусков метод1 все еще был самым быстрым или быстрым, как метод2.

Вот снимок кода:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

и результаты:

Выполнить 1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

Run 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2

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

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

Создание исключения вместо использования простой проверки if..else также усложнит код для написания и сопровождения.

Мое мнение о скорости исключения против проверки данных программно.

Во многих классах есть конвертер строк в значения (сканер / анализатор), уважаемые и известные библиотеки;)

обычно имеет форму

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

имя исключения - только пример, обычно не проверяется (время выполнения), поэтому объявление throws - только моя картинка

иногда существует вторая форма:

public static Example Parse(String input, Example defaultValue)

никогда не бросать

Когда второй недоступен (или программист читает слишком мало документов и использует только первый), напишите такой код с регулярным выражением. Регулярные выражения - это круто, политкорректно и т. Д.

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

с этим кодом программисты не имеют стоимости исключений. НО ИМЕЕТ ВСЕГДА сравнимую очень ВЫСОКУЮ стоимость регулярных выражений с небольшой стоимостью исключения иногда.

Я почти всегда использую в таком контексте

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

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

Не бойся исключений

Почему исключения должны быть медленнее, чем обычно?

Пока вы не печатаете трассировку стека в терминале, не сохраняете ее в файл или что-то подобное, блок catch не выполняет больше работы, чем другие кодовые блоки. Итак, я не могу себе представить, почему "throw new my_cool_error()" должно быть таким медленным.

Хороший вопрос, и я с нетерпением жду дополнительной информации по этой теме!

Другие вопросы по тегам