Вопрос-ответ

What are the effects of exceptions on performance in Java?

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

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

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


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

и


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

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

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

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

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

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

Переведено автоматически
Ответ 1

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

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

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

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

See the following test code:

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 и выбрасываете все в рамках одного метода (method3), но это специальная JIT-оптимизация, на которую я бы не стал полагаться. И даже при использовании этой оптимизации выбрасывание по-прежнему происходит довольно медленно. Так что я не знаю, что вы пытаетесь здесь сделать, но определенно есть лучший способ сделать это, чем использовать try / catch / throw.

Ответ 2

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

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 такие же, как у Mecki (мой ноутбук, очевидно, работает медленнее).

метод4 идентичен методу3, за исключением того, что он создает new Integer(1), а не выполняет throw new Exception().

метод5 похож на метод3, за исключением того, что он создает new Exception(), не выбрасывая его.

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

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

Ответ 3

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


  • Недавно созданные исключения по сравнению с предварительно созданными исключениями

  • Трассировка стека включена или отключена

  • Запрошенная трассировка стека vs никогда не запрашиваемая

  • Пойманный на верхнем уровне vs повторно созданный на каждом уровне vs связанный / обернутый на каждом уровне

  • Различные уровни глубины стека вызовов Java

  • Отсутствие встроенных оптимизаций против экстремальных встроенных настроек по умолчанию

  • Пользовательские поля читаются или не читаются

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

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


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


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


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


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


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


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


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

Ответ 4

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

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

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

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

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

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

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

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

java performance exception