Насколько медленны исключения Java? - PullRequest
447 голосов
/ 18 ноября 2008

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

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

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

и

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

Этот вопрос касается № 1.

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

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

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

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

Ответы [ 17 ]

329 голосов
/ 18 ноября 2008

Зависит от того, как реализованы исключения. Самый простой способ - использовать 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 слишком мало, чтобы исключить мешающие факторы, такие как фоновые процессы. Но блок catch убил все и сделал его в 66 раз медленнее!

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

240 голосов
/ 06 ноября 2011

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

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 большая часть затрат на создание исключения - это время, затрачиваемое на сбор трассировки стека, которое происходит при создании объекта исключения. Фактическая стоимость создания исключения, хотя и велика, значительно меньше стоимости создания исключения.

56 голосов
/ 21 января 2015

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

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

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

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

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

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

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

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

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

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

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

39 голосов
/ 10 апреля 2011

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

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

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

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

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

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

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

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

18 голосов
/ 15 августа 2014

Я расширил ответы, данные @ 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 Спасибо @ Грег

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}
7 голосов
/ 20 октября 2010

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

Итак, Java Throwable, соответственно, имеет собственный метод fillInStackTrace. Я думаю, что описанный выше блок killer- 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 заключается в том, что они генерируют различные виды исключений. Да, method4 все еще медленнее, чем method1 и method2, но разница гораздо более приемлема.

7 голосов
/ 19 ноября 2008

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

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

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

7 голосов
/ 18 ноября 2008

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

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

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

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

6 голосов
/ 18 ноября 2008

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

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

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

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

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

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

3 голосов
/ 18 ноября 2008

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

...