Дорого ли использовать блоки try-catch, даже если исключение не выдается?

189

Мы знаем, что ловить исключения дорого. Но стоит ли также использовать блок try-catch в Java, даже если исключение не выдается?

Я нашел вопрос / ответ о переполнении стека. Почему стоит попробовать блоки? , но это для .NET .

jsedano
источник
30
Там действительно нет смысла в этом вопросе. Try..catch имеет очень конкретное назначение. Если вам это нужно, вам это нужно. В любом случае, какой смысл попробовать без улова?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }законно и полезно
A4L
4
Эти затраты должны быть сопоставлены с выгодами. Это не одиноко. В любом случае, дорогой является относительным, и пока вы не знаете, что вы не можете сделать это, имеет смысл использовать самый очевидный метод, а не делать что-то, потому что это может сэкономить вам миллисекунду или две в течение часа выполнение программы.
Джоэл
4
Я надеюсь, что это не ведет к ситуации типа «
давайте
6
@SAFX: с Java7 вы даже можете избавиться от finallyблока, используяtry-with-resources
a_horse_with_no_name

Ответы:

201

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


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

Потому что в tryблоке может быть сгенерировано исключение (в любой строке блока try! Некоторые исключения генерируются асинхронно, например, путем вызова stopпотока (что устарело), ​​и даже, кроме того, что OutOfMemoryError может произойти почти где угодно), и все же это может быть перехвачен, и код продолжит выполнение впоследствии в том же методе, более трудно рассуждать об оптимизации, которая может быть сделана, поэтому они менее вероятны. (Кто-то должен был бы запрограммировать компилятор, чтобы он делал это, рассуждал и гарантировал правильность и т. Д. Это было бы большой болью для чего-то, что должно было быть «исключительным») Но опять же, на практике вы не заметите таких вещей.

Patashu
источник
2
Некоторые исключения генерируются асинхронно , они не асинхронные, а в безопасных точках. и у этой части попытки есть некоторые незначительные затраты, связанные с этим. Java не может выполнить некоторую оптимизацию кода в блоке try, который в противном случае потребовал бы серьезной ссылки. В какой-то момент код, скорее всего, окажется в блоке try / catch. Возможно, верно, что блок try / catch будет сложнее встроить и построить правильную решетку для результата, но часть с перестановкой неоднозначна.
bestsss
2
Не try...finallyблокирует ли блок catchтакже некоторые оптимизации?
dajood
5
@Patashu "Это на самом деле выбрасывает исключение, которое стоит вам" Технически, выбрасывание исключения не дорого; создание Exceptionобъекта - это то, что занимает большую часть времени.
Остин D
72

Давайте измерим это, не так ли?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

На моем компьютере это печатает что-то вроде:

try     0.598 ns
no try  0.601 ns

По крайней мере, в этом тривиальном примере оператор try не оказал заметного влияния на производительность. Не стесняйтесь измерять более сложные.

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

Meriton
источник
4
в то время как попытка / попытка, скорее всего, будет одинаковой на большинстве JVM, микробенчмарк ужасно ошибочен.
bestsss
2
довольно много уровней: вы имеете в виду, что результаты вычисляются в менее 1 нс? Скомпилированный код удалит оба цикла try / catch и цикл (суммирование числа от 1 до n является тривиальной суммой арифметической прогрессии). Даже если код содержит try / finally, который может доказать компилятор, в него нечего добавить. В абстрактном коде всего 2 сайта вызовов, и он будет клонированным и встроенным. Есть еще случаи, просто посмотрите некоторые статьи по микробенчмарку, и если вы решите написать микробенчмарк, всегда проверяйте сгенерированную сборку.
bestsss
3
Сообщаемое время указывается для каждой итерации цикла. Поскольку измерение будет использоваться только в том случае, если у него есть общее прошедшее время> 0,1 секунды (или 2 миллиарда итераций, чего не было здесь), я считаю ваше утверждение, что цикл был удален полностью, трудно поверить - потому что если цикл был удален, что потребовало 0,1 секунды для выполнения?
Меритон
... и действительно, в соответствии с этим -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblyи цикл, и его дополнение присутствуют в сгенерированном собственном коде. И нет, абстрактные методы не являются встроенными, потому что их вызывающая сторона не только вовремя компилируется (предположительно, потому что она не вызывается достаточно раз).
Меритон
Как мне написать правильный микро-бенчмарк на Java: stackoverflow.com/questions/504103/…
Вадим
47

try/ catchможет оказать некоторое влияние на производительность. Это потому, что это мешает JVM выполнять некоторые оптимизации. Джошуа Блох в «Эффективной Яве» сказал следующее:

• Помещение кода в блок try-catch препятствует определенным оптимизациям, которые иначе могли бы выполнить современные реализации JVM.

Евгений Дорофеев
источник
25
"это мешает JVM выполнять некоторые оптимизации" ...? Не могли бы вы уточнить вообще?
Кракен
5
@ Кракен-код внутри блоков try (обычно «всегда») нельзя переупорядочить с помощью кода вне блоков try, как один из примеров.
Паташу
3
Обратите внимание, что вопрос заключался в том, «дорого ли это», а не «влияет ли это на производительность».
Миколак
3
добавил отрывок из Effective Java , и это, конечно, библия из Java; если нет ссылки, выдержка ничего не говорит. Практически любой код в Java находится в пределах try / finally на некотором уровне.
bestsss
29

Да, как говорили другие, tryблок запрещает некоторые оптимизации для {}окружающих его персонажей. В частности, оптимизатор должен предполагать, что исключение может произойти в любой точке блока, поэтому нет гарантии, что операторы будут выполнены.

Например:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

Без этого tryзначение, рассчитанное для присваивания, xможет быть сохранено как «общее подвыражение» и использовано для присвоения y. Но из-за tryотсутствия уверенности в том, что первое выражение когда-либо оценивалось, поэтому выражение необходимо пересчитать. Обычно это не имеет большого значения в «прямолинейном» коде, но может иметь большое значение в цикле.

Следует отметить, однако, что это относится ТОЛЬКО к JITCed-коду. javac выполняет лишь незначительную оптимизацию, и интерпретатору байт-кода не требуется никаких затрат для входа / выхода из tryблока. (Нет байт-кодов, сгенерированных, чтобы отметить границы блока.)

И для бестссс:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

Вывод:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

вывод javap:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

Нет "GOTO".

Горячие лижет
источник
Нет никаких сгенерированных байт-кодов, чтобы отметить границы блока, это не обязательно - это действительно требует GOTO, чтобы покинуть блок, иначе это попадет в catch/finallyкадр.
bestsss
@bestsss - даже если генерируется GOTO (который не является заданным), его стоимость минимальна, и это далеко не «маркер» для границы блока - GOTO может быть сгенерирован для многих конструкций.
Hot Licks
Я никогда не упоминал стоимость, однако не было сгенерированных байт-кодов, это ложное утверждение. Вот и все. На самом деле, в байт-коде нет блоков, кадры не равны блокам.
bestsss
GOTO не будет, если попытка попадет непосредственно в окончание, и есть другие сценарии, где GOTO не будет. Дело в том, что в порядке байт-кодов "enter try" / "exit try" нет ничего.
Hot Licks
GOTO не будет, если попытка попадет прямо в окончательно - Ложь! нет finallyв байткод, это try/catch(Throwable any){...; throw any;}и это действительно есть улов о ж / фрейме и Throwable , которые должны быть определены (не нулевой) и так далее. Почему вы пытаетесь спорить о теме, вы можете проверить хотя бы какой-нибудь байт-код? Действующее руководство для импл. Наконец, копирование блоков и избегание раздела goto (предыдущий импл), но байт-коды должны быть скопированы в зависимости от того, сколько точек выхода есть.
bestsss
8

Чтобы понять, почему оптимизация не может быть выполнена, полезно понять основные механизмы. Самый краткий пример, который я смог найти, был реализован в макросах C по адресу: http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html.

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

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

technosaurus
источник
4
Эти макросы C, которые вы нашли для try / catch, не эквивалентны Java или реализации C #, которые выдают 0 инструкций времени выполнения.
Паташу
Реализация java слишком обширна, чтобы включать ее полностью, это упрощенная реализация с целью понимания основной идеи о том, как могут быть реализованы исключения. Утверждение, что оно испускает 0 инструкций времени выполнения, вводит в заблуждение. Например, простое classcastexception расширяет runtimeexception, которое расширяет исключение, которое расширяет throwable, которое включает в себя: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... Это все равно, что сказать, что switch-case в C free, если используется только 1 случай, все еще есть небольшие накладные расходы при запуске.
технозавр
1
@Patashu Все эти предварительно скомпилированные биты все еще должны быть загружены при запуске, независимо от того, используются ли они когда-либо. Нет никакого способа узнать, будет ли исключение нехватки памяти во время выполнения во время компиляции - именно поэтому они называются исключениями времени выполнения - в противном случае они будут предупреждениями / ошибками компилятора, так что нет, это не оптимизирует все весь код для их обработки включен в скомпилированный код и имеет стоимость запуска.
технозавр
2
Я не могу говорить о C. В C # и Java попытка реализована путем добавления метаданных, а не кода. Когда вводится блок try, ничего не выполняется, чтобы указать на это - когда генерируется исключение, стек не наматывается, а метаданные проверяются на наличие обработчиков этого типа исключения (дорого).
Паташу
1
Да, я фактически реализовал интерпретатор Java и статический компилятор байт-кода и работал над последующим JITC (для IBM iSeries), и я могу сказать вам, что нет ничего, что «помечает» вход / выход диапазона try в байт-кодах, а скорее диапазоны указаны в отдельной таблице. Интерпретатор не делает ничего особенного для диапазона попытки (пока не будет сгенерировано исключение). JITC (или статический компилятор байт-кода) должен знать о границах для подавления оптимизаций, как указано ранее.
Hot Licks
8

Еще один микробенчмарк ( источник ).

Я создал тест, в котором я измеряю версию кода try-catch и no-try-catch на основе процента исключений. 10% процент означает, что 10% тестовых случаев имели деление на ноль. В одной ситуации он обрабатывается блоком try-catch, в другой - условным оператором. Вот моя таблица результатов:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Процент | Результат (попробуйте / если, нс)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Что говорит о том, что между этими случаями нет существенной разницы.

Андрей Чащев
источник
3

Я нашел ловлю NullPointException довольно дорого. Для операций 1.2k время составляло 200 мс и 12 мс, когда я обрабатывал его таким же образом, if(object==null)что было довольно хорошим для меня.

Матеуш Кафловски
источник