Вопрос: Действительно ли обработка исключений в Java медленная?
Общепринятая мудрость, а также многие результаты Google говорят, что исключительная логика не должна использоваться для нормального выполнения программы на Java. Обычно приводятся две причины:
и
Этот вопрос касается №1.
Например, эта страница описывает обработку исключений Java как «очень медленную» и связывает медленность с созданием строки сообщения об исключении - «эта строка затем используется при создании генерируемого объекта исключения. Это не быстро». В статье Эффективная обработка исключений в Java говорится, что «причина этого связана с аспектом создания объекта при обработке исключений, который, таким образом, существенно замедляет генерирование исключений». Другая причина заключается в том, что генерация трассировки стека - это то, что замедляет его.
Мое тестирование (с использованием Java 1.6.0_07, Java HotSpot 10.0, на 32-битном Linux) показывает, что обработка исключений не медленнее, чем обычный код. Я пробовал запустить метод в цикле, который выполняет некоторый код. В конце метода я использую логическое значение, чтобы указать, возвращаться или бросать. Таким образом, фактическая обработка будет такой же. Я пробовал запускать методы в разном порядке и усреднять время тестирования, думая, что это могло быть разогревом JVM. Во всех моих тестах бросок был как минимум таким же быстрым, как и возврат, если не быстрее (на 3,1% быстрее). Я полностью открыт для возможности того, что мои тесты были неправильными, но я не видел ничего в виде образца кода, сравнений тестов или результатов за последние год или два, которые показывают, что обработка исключений в Java действительно медленный.
По этому пути меня ведет API-интерфейс, который мне нужен, который генерирует исключения как часть нормальной логики управления. Я хотел исправить их в их использовании, но сейчас, возможно, не смогу. Смогу ли я вместо этого хвалить их за их дальновидность?
В статье Эффективная обработка исключений Java при своевременной компиляции авторы предполагают, что наличия только обработчиков исключений, даже если исключения не генерируются, достаточно, чтобы не дать компилятору JIT оптимизировать код должным образом, тем самым замедляя его работу. Я еще не проверял эту теорию.
Билл, утверждая, что использование исключений для выполнения программы не лучше, чем использование GOTO, не лучше, чем утверждение, что использование условных выражений и циклов для выполнения программы не лучше, чем использование GOTO. Это отвлекающий маневр. Объяснись. Исключения могут и эффективно используются для выполнения программы на других языках. Например, идиоматический код Python регулярно использует исключения. Я могу и поддерживал код, который использует исключения таким образом (хотя и не Java), и я не думаю, что в этом есть что-то принципиально неправильное.
Обратите внимание, что некоторые веб-фреймворки используют исключения как удобный способ перенаправления - например, Wicket's RestartResponseException. Это происходит всего несколько раз на запрос, обычно нет, и я не могу представить более удобный способ в Java-ориентированной компонентной структуре.
@mmalone с использованием исключений для нормального потока управления - плохая идея в Java, потому что выбор парадигмы был сделан таким образом. Прочтите Блоха EJ2 - он ясно заявляет, что, цитируя, (пункт 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow - дает полное и подробное объяснение того, почему. И он был тем парнем, который написал Java lib. Следовательно, он тот, кто определяет контракт API классов. Я согласен с Биллом К.
@ OndraŽižka Если некоторая структура делает это (используйте Исключения в неисключительных условиях), она ошибочна и нарушена по дизайну, нарушая контракт класса Exception языка. То, что некоторые люди пишут плохой код, не делает его менее паршивым.
Кстати, меня очень беспокоит то, что комментарий mmalone получил так много голосов, несмотря на то, что он сказал: «Я не думаю, что с ним что-то не так» ... Хотя я понимаю его запрос на ссылку (хотя ссылка в этом случае довольно очевидна ), мне грустно видеть, что люди соглашаются с этим, поскольку это только подчеркивает, насколько сильно для меня ТАК "обеспечение качества сообщества" со временем ухудшилось.
Никто, кроме создателя stackoverflow.com, не заявляет, что исключения хуже, чем GOTO: joelonsoftware.com/items/2003/10/13.html
Никто, кроме создателя stackoverflow.com, не ошибается в отношении исключений. Золотое правило разработки программного обеспечения - никогда не делать простое сложным и громоздким. Он пишет: «Это правда, что то, что должно быть простой трехстрочной программой, часто превращается в 48 строк, когда вы добавляете хорошую проверку ошибок, но это жизнь…» Это поиск чистоты, а не простоты.
Одна из забавных вещей в Java - мы озабочены чем-то вроде производительности try / catch, которая замедляет Java в ОГРОМНЫЙ фактор - но она все равно будет быстрее, чем почти любой другой язык (кроме C и несколько эзотерических вроде Фортрана!?!). Даже если вы интенсивно используете исключения, вы не снизите скорость до скорости Python; а насчет Руби ??? Даже если бы каждый оператор был каким-то образом основан на исключениях, вы не смогли бы сделать ТАК медленно. С другой стороны, исключения приводят к тому, что код трудно отслеживать, и, как правило, они являются PITA, которых лучше избегать, когда это возможно.
Раньше заполнение трассировки стека было довольно медленным. Насколько я понимаю, это значительно улучшилось вокруг java 5-6-ish
OP должен запускаться после загрузки тысячи кадров в стек, поэтому влияние более крупной трассировки стека будет более очевидным.
Вопрос касается «обработки» исключений, где альтернативой очень часто является сбой. Вы не можете работать намного медленнее, чем остановились. Если вы думаете об использовании исключений в качестве элемента управления потоком, как предложил Билл К., просто убедитесь, что никто другой не должен поддерживать код ...




Даже если генерирование исключения не является медленным, все же плохая идея генерировать исключения для нормального выполнения программы. Используемый таким образом он аналогичен GOTO ...
Я думаю, это не совсем ответ на вопрос. Я полагаю, что «обычная» мудрость создания медленных исключений была верна в более ранних версиях java (<1.4). Для создания исключения требуется, чтобы виртуальная машина создала всю трассировку стека. С тех пор многое изменилось в виртуальной машине, чтобы ускорить процесс, и, вероятно, это одна из областей, которая была улучшена.
Было бы хорошо определить «нормальный ход программы». Много было написано об использовании проверенных исключений в качестве сбоя бизнес-процесса и непроверенного исключения для неисправимых сбоев, поэтому в определенном смысле сбой в бизнес-логике все еще можно рассматривать как нормальный поток.
@Spencer K: Исключение, как следует из названия, означает, что была обнаружена исключительная ситуация (файл исчез, сеть внезапно закрылась, ...). Это означает, что ситуация была НЕОЖИДАННОЙ. Если ОЖИДАЕТСЯ, что такая ситуация возникнет, я бы не стал использовать для нее исключение.
@Mecki: верно. Я недавно с кем-то обсуждал это ... Они писали фреймворк валидации и выдавали исключение в случае неудачной валидации. Я думаю, что это плохая идея, так как это довольно распространенное явление. Я бы предпочел, чтобы метод возвращал ValidationResult.
@Mecki: Ну, Throwable - это суперкласс Exception, и название Метательный не означает, что произошло что-то необычное.
С точки зрения потока управления исключение аналогично break или return, но не goto.
Существует множество парадигм программирования. Не может быть единого «нормального потока», что бы вы под этим ни подразумевали. По сути, механизм исключения - это просто способ быстро покинуть текущий кадр и раскрутить стек до определенного момента. Слово «исключение» ничего не говорит о его «неожиданном» характере. Быстрый пример: очень естественно «выбрасывать» сообщения 404 из веб-приложений, когда на пути маршрутизации возникают определенные обстоятельства. Почему бы не реализовать эту логику с исключениями? Что за антипаттерн?
Почему исключения должны возвращаться медленнее, чем обычно?
Пока вы не распечатываете трассировку стека на терминал, не сохраняете ее в файл или что-то подобное, блок catch не выполняет больше работы, чем другие блоки кода. Итак, я не могу представить, почему throw new my_cool_error () должен быть таким медленным.
Хороший вопрос, и я с нетерпением жду дополнительной информации по этой теме!
Исключение должно захватить информацию о трассировке стека, даже если она фактически не используется.
Я думаю, что в первой статье процесс обхода стека вызовов и создания трассировки стека рассматривается как дорогостоящая часть, и хотя во второй статье об этом не говорится, я думаю, что это самая дорогая часть создания объекта. У Джона Роуза есть статья, в которой он описывает различные методы ускорения исключений. (Предварительное выделение и повторное использование исключения, исключения без трассировки стека и т. д.)
Но все же - я считаю, что это следует рассматривать только как необходимое зло, в крайнем случае. Причина, по которой Джон сделал это, - имитировать функции на других языках, которые (пока) недоступны в JVM. Вы НЕ должны иметь привычки использовать исключения для потока управления. Особенно по соображениям производительности! Как вы сами упомянули в № 2, вы рискуете таким образом замаскировать серьезные ошибки в своем коде, и новым программистам будет труднее поддерживать его.
Микробенчмарки в Java на удивление сложно сделать правильно (мне сказали), особенно когда вы попадаете на территорию JIT, поэтому я действительно сомневаюсь, что использование исключений быстрее, чем «возврат» в реальной жизни. Например, я подозреваю, что в вашем тесте где-то между 2 и 5 кадрами стека? Теперь представьте, что ваш код будет вызываться компонентом JSF, развернутым JBoss. Теперь у вас может быть трассировка стека длиной в несколько страниц.
Возможно, вы могли бы опубликовать свой тестовый код?
HotSpot вполне может удалить код исключения для исключений, сгенерированных системой, если он полностью встроен. Однако явно созданные исключения и те, которые не были удалены иным образом, тратят много времени на создание трассировки стека. Переопределите fillInStackTrace, чтобы увидеть, как это может повлиять на производительность.
Я провел некоторое тестирование производительности с JVM 1.5, и использование исключений было как минимум в 2 раза медленнее. В среднем: время выполнения тривиально небольшого метода увеличилось более чем втрое (3 раза) с исключениями. Тривиально маленький цикл, который должен был перехватить исключение, показал двукратное увеличение собственного времени.
Я видел похожие числа в производственном коде, а также в микро-тестах.
Исключения обязательно следует использовать НЕТ для всего, что часто вызывается. Выбрасывание тысячи исключений в секунду привело бы к огромному горлышку бутылки.
Например, использование «Integer.ParseInt (...)» для поиска всех неверных значений в очень большом текстовом файле - очень плохая идея. (Я видел производительность этого служебного метода убийство в производственном коде)
Использование исключения для сообщения о неверном значении в форме пользовательского графического интерфейса, вероятно, не так уж и плохо с точки зрения производительности.
Независимо от того, является ли это хорошей практикой проектирования, я бы придерживался правила: если ошибка нормальная / ожидаемая, используйте возвращаемое значение. Если это ненормально, используйте исключение. Например: чтение пользовательского ввода, неверные значения - это нормально - используйте код ошибки. Передавая значение внутренней служебной функции, неверные значения должны быть отфильтрованы вызывающим кодом - используйте исключение.
Позвольте мне предложить некоторые вещи, которые ДОЛЖНЫ делать: если вам нужно число в форме, вместо использования Integer.valueOf (String) вам следует подумать об использовании сопоставителя регулярных выражений. Вы можете предварительно скомпилировать и повторно использовать шаблон, поэтому создание сопоставлений будет дешевым. Однако в форме GUI наличие isValid / validate / checkField или то, что у вас есть, вероятно, более четкое. Кроме того, в Java 8 есть необязательные монады, поэтому подумайте об их использовании. (ответ 9 лет, но все же!: p)
Это зависит от того, как реализованы исключения. Самый простой способ - использовать setjmp и longjmp. Это означает, что все регистры ЦП записываются в стек (что уже занимает некоторое время) и, возможно, необходимо создать некоторые другие данные ... все это уже происходит в инструкции try. Оператор throw должен раскрутить стек и восстановить значения всех регистров (и возможные другие значения в виртуальной машине). Таким образом, try и throw одинаково медленны, и это довольно медленно, однако, если исключение не генерируется, выход из блока try в большинстве случаев не требует времени вообще (поскольку все помещается в стек, который очищается автоматически, если метод существует).
Sun и другие признали, что это, возможно, неоптимально, и, конечно, виртуальные машины со временем становятся все быстрее и быстрее. Есть еще один способ реализовать исключения, который заставляет пробовать себя молниеносно (на самом деле ничего не происходит с попыткой вообще - все, что должно произойти, уже сделано, когда класс загружается виртуальной машиной), и это делает бросок не таким медленным . Я не знаю, какая JVM использует эту новую, лучшую технику ...
... но пишете ли вы на Java, чтобы впоследствии ваш код работал только на одной JVM в одной конкретной системе? Поскольку, если он может когда-либо работать на любой другой платформе или любой другой версии JVM (возможно, от любого другого поставщика), кто сказал, что они также используют быструю реализацию? Быстрый более сложный, чем медленный, и не всегда возможен во всех системах. Вы хотите оставаться портативным? Тогда не рассчитывайте, что исключения будут быстрыми.
Также большое значение имеет то, что вы делаете в блоке try. Если вы открываете блок try и никогда не вызываете какой-либо метод из этого блока try, блок try будет очень быстрым, так как JIT может фактически обрабатывать throw как простой goto. Ему не нужно ни сохранять состояние стека, ни раскручивать стек при возникновении исключения (ему нужно только перейти к обработчикам catch). Однако это не то, что вы обычно делаете. Обычно вы открываете блок 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.
отличный вопрос и ответ, спасибо John & Mecki! Я унаследовал код, который пытается поймать любой метод (в том числе сеттер / получатель, который просто возвращает значение свойства), и проверял некоторые дополнительные сведения о последствиях этого для производительности. Вопрос и ответ дали мне полную информацию об этом;). С уважением, Стеф.
@Mecki Каким образом ваша производительность так сильно пострадала, когда точка доступа JDK должна кэшировать эти исключения. javaspecialists.eu/archive/Issue187.html Вы отключили эту опцию для запуска ваших тестов?
Ах, после долгой отладки, я думаю, когда код генерирует новое исключение через throw new Exception(), JDK не кэширует его. Но если Java бросит его, например ((Object)null).getClass(), то java его кеширует.
Честно говоря, должна существовать версия подпрограммы, которая создает объект (возможно, исключение), не генерируя ничего, чтобы увидеть, насколько разница в производительности связана с простым созданием объекта.
(Результаты такого теста я опубликовал ниже.)
Отличный ответ, но я просто хотел бы добавить, что, насколько мне известно, для измерения производительности следует использовать System.nanoTime (), а не System.currentTimeMillis ().
@ SimonAndréForsberg nanoTime() требует Java 1.5, и у меня была только Java 1.4, доступная в системе, которую я использовал для написания кода выше. На практике это тоже не играет большой роли. Единственное различие между ними состоит в том, что один составляет наносекунду, а другой - одну миллисекунду, и что на nanoTime не влияют манипуляции с часами (которые не имеют значения, если вы или системный процесс не изменяете системные часы точно в момент выполнения тестового кода). Хотя в целом вы правы, nanoTime, конечно, лучший выбор.
Следует отметить, что ваш тест - это крайний случай. Вы демонстрируете очень небольшое снижение производительности для кода с блоком try, но без throw. Ваш тест throw выдает исключения 50% времени он проходит через try. Это явно ситуация, когда отказ не исключительный. Уменьшение этого показателя до 10% значительно снижает производительность. Проблема с такого рода тестами заключается в том, что они побуждают людей вообще отказаться от использования исключений. Использование исключений для обработки исключительных случаев работает намного лучше, чем показывает ваш тест.
@Nate Прежде всего, я очень четко сказал, что все это зависит от того, как реализованы исключения. Я просто тестировал ОДНУ конкретную реализацию, но их много, и Oracle может выбирать совершенно другую с каждым выпуском. Во-вторых, если исключения являются только исключительными, каковыми они обычно являются, конечно, влияние меньше, это настолько очевидно, что я действительно не думаю, что кто-то должен указывать на это явно, и поэтому я вообще не могу понять вашу точку зрения. И в-третьих, исключение - злоупотребление им плохо, все согласны с этим, поэтому осторожное их использование - это очень хорошо.
Меки прав. Вопрос заключался в сравнении относительной скорости между обычным потоком управления (оператор возврата) и исключениями. Во всяком случае, исключения следует выдавать в 100% случаев, а не только в 50%. Это означает, что мы могли бы говорить о коде в 132 раза медленнее!
ИМО, еще одно сравнение может быть интересным: «throw new Exception ()» и «new Exception ()», таким образом мы можем измерить саму операцию «throw», а не постоянно смешивать ее с «new Exception ()». WDYT?
@Mecki Извините, не могли бы вы немного пояснить свой ответ? Я не понимаю, почему метод 3 работает намного медленнее. Когда вы вызываете метод, который вызывает другой метод, он использует стек для отслеживания вызовов независимо от того, находится ли он внутри try / catch, верно? Так что же заставляет пребывать внутри «пытаться / ловить» медленнее? Спасибо.
@Glide Есть два способа реализовать try/catch. Либо try сохраняет все состояние обработки потока, и если возникает исключение, состояние которого восстанавливается до выполнения catch, это замедляет работу try (независимо от того, генерируется ли исключение когда-либо или нет), но catch не будет таким медленным. Или try записывает только обработчик catch, а обработчик catch обнаруживает, какие операции он должен отменить (подумайте об откате), что делает try очень быстрым, но catch очень медленным (однако catch всегда выполняется, только если действительно выбрасывается исключение) . Извините, нужно было бы написать книгу для всех подробностей.
@Glide Бросок не похож на чистый return. Он оставляет метод где-то в середине тела, может быть, даже в середине операции (которая пока завершена только на 50%), а блок catch может быть на 20 кадров стека вверх (метод имеет блок try, вызывающий method1, который вызывает method2, который вызывает mehtod3, ..., а в method20 в середине операции генерируется исключение). Стек должен быть развернут на 20 кадров вверх, все незавершенные операции должны быть отменены (операции не должны выполняться наполовину), а регистры ЦП должны быть в чистом состоянии. Все это требует времени.
Вы также должны попробовать протестировать его при выдаче исключений без трассировки стека.
на jdk 1.8.0_181 я получаю эти результаты, показывающие, что производительность улучшилась: метод 1 занял 928 мс, результат был 2 | method2 занял 1098 мс, результат был 2 | method3 занял 26366 мс, результат был 2
В более поздних версиях Java этот тест ошибочен и не разогревает код. Если вы добавите цикл вокруг кода в main для многократного запуска теста, не будет существенной разницы ни в одном из решений.
@ john16384 Если я добавлю цикл вокруг main и провожу тест 3 раза с Java 8, я получу в три раза одинаковые результаты и в три раза медленнее при возникновении исключения, поэтому я не могу подтвердить ваше утверждение. Единственное, что изменилось, это то, что try больше не имеет штрафа за скорость, но, как я также объяснил в своем ответе, это зависит от того, как реализованы исключения. Можно реализовать try таким образом, чтобы не выполнялся дополнительный код, если только не было фактически сгенерировано исключение, но, следовательно, эта реализация делает выброс более дорогостоящим, чем альтернативы.
@Mecki См. Этот ответ, чтобы понять, что я имею в виду: stackoverflow.com/a/66320622/1262865 - также более сложное тестирование (где я использую Exception, чтобы отметить особый случай) показывает аналогичное поведение: статическое (повторно используемое) исключение, которое всегда перехватывается, работает точно так же как «особое» значение (null). Нестатический режим работает примерно в 6 раз хуже.
Некоторое время назад я написал класс для проверки относительной производительности преобразования строк в целые числа, используя два подхода: (1) вызвать Integer.parseInt () и перехватить исключение или (2) сопоставить строку с регулярным выражением и вызвать parseInt () только в случае удачного совпадения. Я использовал регулярное выражение наиболее эффективным способом (т. Е. Создавая объекты Pattern и Matcher перед переходом в цикл), и я не печатал и не сохранял трассировки стека из исключений.
Для списка из десяти тысяч строк, если бы все они были действительными числами, подход parseInt () был в четыре раза быстрее, чем подход регулярного выражения. Но если только 80% строк были действительными, регулярное выражение было в два раза быстрее, чем parseInt (). И если 20% были действительными, то есть исключение было сгенерировано и перехвачено в 80% случаев, регулярное выражение было примерно в двадцать раз быстрее, чем parseInt ().
Я был удивлен результатом, учитывая, что подход регулярных выражений обрабатывает действительные строки дважды: один раз для совпадения и еще раз для parseInt (). Но выброс и перехват исключений с лихвой компенсировали это. Такая ситуация вряд ли будет часто возникать в реальном мире, но если это произойдет, вам определенно не следует использовать технику перехвата исключений. Но если вы только проверяете ввод пользователя или что-то в этом роде, обязательно используйте подход parseInt ().
какую JVM вы использовали? это все еще так медленно с sun-jdk 6?
Я откопал его и снова запустил под JDK 1.6u10, прежде чем отправить этот ответ, и это результаты, которые я опубликовал.
Это очень и очень полезно! Спасибо. Для моих обычных случаев использования мне нужно анализировать вводимые пользователем данные (используя что-то вроде Integer.ParseInt()), и я ожидаю, что в большинстве случаев ввод пользователя будет правильным, поэтому для моего варианта использования кажется, что случайное попадание исключения - это выход.
Не знаю, связаны ли эти темы, но однажды я хотел реализовать один трюк, полагаясь на трассировку стека текущего потока: я хотел узнать имя метода, который запускал создание экземпляра внутри экземпляра класса (да, идея безумная, Я полностью отказался от этого). Итак, я обнаружил, что вызов 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, но разница гораздо более приемлемая.
Просто сравните, скажем, 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 раз), разница в производительности должна стать существенной.
К сожалению, мой ответ слишком длинный, чтобы публиковать его здесь. Итак, позвольте мне подвести итог и отослать вас к http://www.fuwjax.com/how-slow-are-java-exceptions/ за подробностями.
Настоящий вопрос здесь не в том, «Насколько медленны« сбои, сообщаемые как исключения »по сравнению с« кодом, который никогда не дает сбоев »?» как вы могли поверить принятому ответу. Вместо этого следует задать вопрос: «Насколько медленны« отказы, сообщаемые как исключения »по сравнению с отказами, сообщаемыми другими способами?» Как правило, два других способа сообщения об ошибках - либо с помощью контрольных значений, либо с помощью оболочек результатов.
Значения Sentinel - это попытка вернуть один класс в случае успеха и другой - в случае неудачи. Вы можете думать об этом почти как о возврате исключения, а не о его генерации. Для этого требуется общий родительский класс с объектом успеха, а затем выполнение проверки «instanceof» и пары приведений, чтобы получить информацию об успехе или неудаче.
Оказывается, что рискуя безопасностью типов, значения Sentinel быстрее, чем исключения, но только примерно в 2 раза. Может показаться, что это много, но это вдвое покрывает только разницу в реализации. На практике этот коэффициент намного ниже, поскольку наши методы, которые могут потерпеть неудачу, намного интереснее, чем несколько арифметических операторов, как в примере кода в другом месте на этой странице.
С другой стороны, Result Wrappers вообще не жертвуют безопасностью типов. Они объединяют информацию об успехах и неудачах в один класс. Таким образом, вместо instanceof они предоставляют isSuccess () и геттеры для объектов успешного и неудачного выполнения. Однако результирующие объекты примерно в 2 раза больше помедленнее, чем при использовании исключений. Оказывается, каждый раз создавать новый объект-оболочку намного дороже, чем иногда генерировать исключение.
Вдобавок ко всему, исключения - это язык, на котором указывается, что метод может дать сбой. Нет другого способа определить с помощью только API, какие методы, как ожидается, будут всегда (в основном) работать, а какие - сообщать об ошибках.
Исключения безопаснее, чем дозорные, быстрее, чем объекты результата, и менее удивительны, чем любой другой. Я не предлагаю, чтобы try / catch заменяли if / else, но исключения - это правильный способ сообщить об ошибке, даже в бизнес-логике.
Тем не менее, я хотел бы отметить, что два наиболее частых способа существенного влияния на производительность, с которыми я столкнулся, - это создание ненужных объектов и вложенных циклов. Если у вас есть выбор между созданием исключения или не созданием исключения, не создавайте исключение. Если у вас есть выбор между созданием исключения иногда или постоянным созданием другого объекта, создайте исключение.
Я решил протестировать долгосрочную производительность трех реализаций по сравнению с реализацией управления, которая проверяет наличие сбоев без создания отчетов. Уровень отказов процесса составляет около 4%. Итерация теста вызывает процесс 10000 раз против одной из стратегий. Каждая стратегия тестируется 1000 раз, и последние 900 раз используются для генерации статистики. Вот среднее время в нано: Control 338 Exception 429 Result 348 Sentinel 345
Ради интереса я отключил fillInStackTrace в тесте исключения. Вот время: Control 347 Exception 351 Result 364 Sentinel 355
Fuwjax, если мне что-то не хватает (и я признаю, что прочитал только ваш пост SO, а не ваш пост в блоге), похоже, что ваши два комментария выше противоречат вашему сообщению. Я полагаю, что меньшие числа лучше в вашем тесте, верно? В этом случае создание исключений с включенным fillInStackTrace (что является обычным и стандартным поведением по умолчанию) приводит к снижению производительности по сравнению с двумя другими описанными вами методами. Я что-то упустил, или вы действительно прокомментировали свое сообщение, чтобы опровергнуть его?
@Fuwjax - способ избежать выбора "рок-н-ролл", который вы здесь представляете, - это сделать предварительно выделить объектом, который представляет "успех". Обычно можно также заранее выделить объекты для общих случаев отказа. Затем только в редких случаях передачи дополнительных деталей создается новый объект. (Это объектно-ориентированный эквивалент целочисленных «кодов ошибок», плюс отдельный вызов для получения сведений о последней ошибке - метод, существующий десятилетиями.)
@Fuwjax Значит, создание исключения не создает объект в вашей учетной записи? Не уверен, что понимаю это рассуждение. Создаете ли вы исключение или возвращаете объект результата, вы создаете объекты. В этом смысле объекты результата не медленнее, чем выброс исключения.
@ToolmakerSteve Flyweights великолепны, когда они имеют смысл. Обсуждение исключений как объектов ответа на сбой имеет тенденцию вращаться вокруг желания использовать их как «объекты», а не просто как «реакцию на сбой». Итак, как вы указываете на нетривиальный отказ, что, как я понимаю, вы бы назвали необычными случаями отказа?
@FelixGV числа были предназначены как вспомогательные детали. Большинство микротестов по сбоям терпят неудачу каждый раз в цикле. Мне было интересно узнать о долгосрочной производительности в условиях отказа от слабого до умеренного, и я подумал, что это достаточно интересно, чтобы поделиться им. Предположительно, обсуждение производительности исключений означает, что вы настраиваете свой код; на мой взгляд, отключение трассировки стека в производительном коде менее рискованно, чем оболочки результатов или Sentinels, при этом оставаясь столь же производительным.
@Matthias Создание исключения, очевидно, создает объект. Но спасибо, что указали на то, что я непонятен. Вышеупомянутый термин "объект результата" лучше понимать как "оболочка результата". Итак, в дополнение к любому объекту ответа на сбой вы должны создать Result Wrapper. Это дополнительная инстанциация оболочки, о которой я имел в виду. Хорошим примером на практике является новый объект Java 8 Optional.
К вашему сведению, я расширил эксперимент, который сделал Меки:
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 большая часть затрат на создание исключения - это время, затрачиваемое на сбор трассировки стека, которая происходит при создании объекта исключения. Фактические затраты на создание исключения, хотя и велики, значительно меньше затрат на создание исключения.
+1 Ваш ответ касается основной проблемы - времени, необходимого для раскрутки и отслеживания стека, и, во-вторых, выброса ошибки. Я бы выбрал это как окончательный ответ.
красивый. ~ 70% создают исключение, ~ 30% выбрасывают. хорошая информация.
Аналогичный вопрос, сколько дополнительных накладных расходов возникает при перехвате исключения, повторном вызове его и повторном перехвате? Спасибо.
@Basil - Вы должны понять это по приведенным выше цифрам.
Это может зависеть от конкретной реализации. Какая версия Java использовалась для этих тестов?
@ ThorbjørnRavnAndersen - Сейчас у меня есть версия java "1.7.0_65" Java (TM) SE Runtime Environment (сборка 1.7.0_65-b19) 64-разрядная серверная виртуальная машина Java HotSpot (TM) (сборка 24.65-b04, смешанный режим). Неизвестно, что у меня было 5+ лет назад. Но код показан выше, в сообщении Меки. Можете попробовать сами.
@HotLicks, и именно поэтому важно указать, какая версия Java использовалась в сообщении.
@ ThorbjørnRavnAndersen - Так ты сам пробовал? Единственный способ обойти узкое место в производительности - это как-то отложить выполнение большей части new Exception(), но при этом гарантировать, что информация стека не будет потеряна за это время. Это, вероятно, будет дороже, чем прямой подход, в случае, когда на самом деле сигнализируется исключение.
Мы можем отметить, что в стандартном коде создание и выдача исключений происходит в редких случаях (я имею в виду во время выполнения), если это не так, либо условия выполнения очень плохие, либо сама конструкция является проблемой; в обоих случаях производительность не имеет значения ...
Я изменил ответ @Mecki выше, чтобы метод 1 возвращал логическое значение и проверку в вызывающем методе, поскольку вы не можете просто заменить исключение ничем. После двух запусков method1 все еще был либо самым быстрым, либо таким же быстрым, как method2.
Вот снимок кода:
// 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
Бег 2
method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2
Производительность исключений в Java и C# оставляет желать лучшего.
Как программистов, это заставляет нас жить по правилу «исключения должны возникать нечасто» просто по практическим соображениям производительности.
Однако, как ученые-информатики, мы должны восстать против этого проблемного состояния. Человек, создающий функцию, часто не знает, как часто она будет вызываться и вероятен ли успех или неудача. Эта информация есть только у вызывающего абонента. Попытка избежать исключений приводит к нечетким идомам API, где в некоторых случаях у нас есть только чистые, но медленные версии исключений, а в других случаях - быстрые, но неуклюжие ошибки возвращаемого значения, а в других случаях мы получаем оба . Разработчику библиотеки, возможно, придется написать и поддерживать две версии API, а вызывающий должен решить, какую из двух версий использовать в каждой ситуации.
Это какой-то беспорядок. Если бы исключения имели лучшую производительность, мы могли бы избежать этих неуклюжих идиом и использовать исключения, поскольку они предназначены для использования ... как средство структурированного возврата ошибок.
Мне бы очень хотелось, чтобы механизмы исключения реализовывались с использованием методов, более близких к возвращаемым значениям, чтобы мы могли иметь производительность ближе к возвращаемым значениям ... поскольку это то, к чему мы возвращаемся в чувствительном к производительности коде.
Вот образец кода, который сравнивает производительность исключения с производительностью, возвращаемой ошибкой.
public class 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 РАЗ медленнее.
Это наводит на мысль, что, возможно, мы делаем неправильные компромиссы в наших реализациях исключений. Исключения могут быть быстрее, либо избегая дорогостоящих обходов, либо просто превращая их в проверку возвращаемого значения, поддерживаемую компилятором. Пока они этого не сделают, мы вынуждены избегать их, когда хотим, чтобы наш код выполнялся быстро.
Я расширил ответы, данные @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);
}
}
интересно, спасибо. Вот объявление отсутствующего класса: public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
в начале вы написали With Java 7+, we can use, но позже вы написали Output with Java 1.6.0_45,, так что это результат Java 6 или 7?
@WBAR из Java 7, нам просто нужно использовать конструктор Throwable с аргументом boolean writableStackTrace. Но этого нет в Java 6 и ниже. Вот почему я дал индивидуальную реализацию для Java 6 и ниже. Таким образом, приведенный выше код предназначен для Java 6 и ниже. Пожалуйста, внимательно прочтите 1-ю строку 2-го абзаца.
@manikanta «ИМХО, мы не можем разработать четкий API, просто используя возвращаемые типы как для успешных, так и для ошибочных потоков.» - мы можем, если будем использовать Optionals / Results / Maybe, как это делают многие языки.
@Hejazzman Я согласен. Но Optional или аналогичный продукт появился на Java с некоторым опозданием. До этого мы также использовали объекты-оболочки с флагами успеха / ошибки. Но это кажется немного взломом и не кажется мне естественным.
Боюсь, что этот тест ошибочен, по крайней мере, на последних версиях Java. Добавление цикла в основной метод для многократного запуска теста покажет, что все 5 методов занимают примерно одинаковое количество времени после первого цикла.
Алексей Шипилев сделал очень тщательный анализ, в котором он тестировал исключения Java при различных комбинациях условий:
Он также сравнивает их с производительностью проверки кода ошибки на различных уровнях частоты ошибок.
Выводы (дословно цитируемые из его сообщения) были:
Поистине исключительные исключения прекрасно работают. Если вы используете их так, как задумано, и сообщаете только действительно исключительные случаи среди подавляюще большого количества неисключительных случаев, обрабатываемых обычным кодом, то использование исключений является выигрышем в производительности.
Затраты на производительность исключений состоят из двух основных компонентов: построение трассировки стека, когда создается экземпляр Exception, и разматывание стека во время выброса исключения.
Затраты на построение трассы стека пропорциональны глубине стека. в момент создания исключения. Это уже плохо, потому что кто на Земле знает, на какой глубине стека будет вызван этот метод бросания? Даже если вы отключите генерацию трассировки стека и / или кэшируете исключения, вы можете избавиться только от этой части затрат на производительность.
Затраты на раскручивание стека зависят от того, насколько нам повезет с приближением обработчика исключений в скомпилированном коде. Тщательное структурирование кода, чтобы избежать глубокого поиска обработчиков исключений, вероятно, поможет нам стать более удачливыми.
Если мы устраним оба эффекта, то затраты на производительность исключений будут такими же, как у местного филиала. Как бы красиво это ни звучало, это не означает, что вы должны использовать исключения в качестве обычного потока управления, потому что в этом случае вы во власти оптимизирующего компилятора! вы должны использовать их только в действительно исключительных случаях, когда частота исключения амортизирует - это возможные несчастливые затраты на повышение фактического исключение.
Оптимистичное практическое правило выглядит так: частота 10 ^ -4 для исключений достаточно исключительна. Это, конечно, зависит от веса самих исключений, точных действий, предпринятых в обработчиках исключений и т. д.
В результате, когда исключение не генерируется, вы не платите никаких затрат, поэтому, когда исключительное условие достаточно редкое, обработка исключений происходит быстрее, чем использование if каждый раз. Весь пост стоит прочитать.
Мое мнение о скорости исключений по сравнению с программной проверкой данных.
Многие классы имели преобразователь String в значение (сканер / парсер), а также уважаемые и известные библиотеки;)
обычно имеет форму
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
{
}
без анализа трассировки стека и т. д., я считаю, что после Ваших лекций довольно быстро.
Не бойтесь исключений
Отличный пост о производительности исключений:
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).
Переписав поток управления приложением, избегая исключений (возвращая ошибку как return), улучшите производительность вашего приложения в 10-100 раз, в зависимости от того, как часто вы их бросаете))
Сообщение отличное, однако ваш вывод о том, что Hibernate / Spring / EE работает медленно из-за исключений, не основан на том, что вы здесь предоставили. Если ваше приложение Hibernate / Spring полностью загружает процессор, то это будет мощь. Однако гораздо более вероятно, что это что-то другое. Другими причинами низкой производительности является полное отсутствие понимания того, что Hibernate делает под капотом, и что использование ORM волшебным образом не означает, что вы получаете хорошую производительность без тщательной проверки того, не являются ли выполняемые им операторы SQL (и их количество) ужасно неэффективными. .
Используя прилагаемый код на 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);
}
}
}
Но какое это имеет отношение? Мой тест был не для того, чтобы доказать, что исключения работают медленно, а просто для проверки того, есть ли они. Я поделился своими результатами только как образец вывода, а не для доказательства какой-либо точки зрения. Теперь они могут быть реализованы по-другому, ну, как я объяснил, есть много способов их реализовать. Как это фальсифицирует все, что я написал в своем ответе? Какая часть первого предложения моего ответа была неясной, когда я сказал, что это зависит от обстоятельств? Это также зависит от системы, код JVM не идентичен для всех систем, и все, что я сказал, это «не полагайтесь на то, что они быстрые». Так что я скучаю по твоей мысли здесь
Я пришел сюда в поисках ответа на вопрос. Когда я обнаруживаю, что что-то в текущем ответе больше не является точным для недавнего оборудования / версий, я обычно оставляю комментарий, чтобы другие могли быть лучше информированы.
Я знаю, что вы не спрашивали о 2), но вы действительно должны признать, что использование исключения для потока программы не лучше, чем использование GOTO. Некоторые люди защищают gotos, некоторые будут защищать то, о чем вы говорите, но если вы спросите кого-то, кто реализовал и поддерживал любой из них в течение определенного периода времени, они скажут вам, что оба плохо поддерживают методы проектирования (и, вероятно, будут проклинать имя человека, который считал себя достаточно умным, чтобы принять решение использовать их).