Чи дорого використовувати пробні блоки, навіть якщо виняток ніколи не кидається?


189

Ми знаємо, що зловити винятки дорого. Але хіба також дорого використовувати блок Java-спроб-лову на Java, навіть якщо виняток ніколи не кидається?

Я знайшов питання / відповідь щодо переповнення стека Чому спробують блоки дорого? , але це для .NET .


30
В цьому питанні насправді немає сенсу. Спробуйте .. ловити має цілком конкретне призначення. Якщо вона вам потрібна, вона вам потрібна. У будь-якому випадку, який момент - це спроба без улову?
JohnFx

49
try { /* do stuff */ } finally { /* make sure to release resources */ }є законним та корисним
A4L

4
Цю вартість слід зважити на користь. Це не стоїть самотньо. У будь-якому випадку, дороге відносне, і поки ви не дізнаєтесь, що не можете цього зробити, має сенс використовувати найбільш очевидний метод, а не робити щось, тому що це може заощадити мільйон або дві протягом години виконання програми.
Джоель

4
Я сподіваюся, що це не спричинить ситуацію типу "давайте знову винаходимо коди помилок" ...
mikołak

6
@SAFX: за допомогою Java7 ви навіть можете позбутися від finallyблоку, використовуючиtry-with-resources
a_horse_with_no_name

Відповіді:


201

tryмайже не має жодних витрат. Замість того, щоб виконувати роботу зі встановлення під час tryвиконання, метадані коду структуруються під час компіляції таким чином, що коли буде викинуто виняток, він виконує відносно дорогу операцію підйому стека та перегляду того, чи tryіснують блоки, які б зафіксували це виняток. З точки зору неспеціаліста, tryможе бути і вільним. Це насправді викидання виключення, яке коштує вам, але якщо ви не кинете сотні чи тисячі винятків, ви все одно не помітите вартість.


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

Тому що в tryблоці може бути викинуто виняток (у будь-якому рядку блоку спробу! Деякі винятки викидаються асинхронно, наприклад, за допомогою виклику stopна Thread (який застаріло), і навіть крім цього OutOfMemoryError може траплятися майже де завгодно), і все ж це можна впіймати і код продовжувати виконувати згодом тим самим методом, складніше міркувати про оптимізації, які можна зробити, тому вони рідше трапляються. (Хтось повинен запрограмувати компілятор, щоб він їх робив, міркував і гарантував правильність і т. Д. Це було б великим болем за те, що означало бути "винятковим") Але знову ж таки, на практиці ви не помітите подібних речей.


2
Деякі винятки кидаються асинхронно , вони не асинхронізуються, а кидаються у безпечні точки. і ця частина спроби пов'язана з деякими незначними витратами. Java не може виконати деякі оптимізації коду в блоці спробу, що в іншому випадку потрібно серйозної посилання. В якийсь момент код, швидше за все, знаходиться в блоці спробу / лову. Це може бути правдою, що блок "try / catch" буде важче окреслити і створити належну решітку для результату, але частина w / перестановка неоднозначна.
bestsss

2
Чи try...finallyблокує без catchзапобігання певні оптимізації?
dajood

5
@Patashu "Це насправді кидання винятку, який коштує тобі" Технічно кидати виняток не дорого; інстанціювання Exceptionоб'єкта - це те, що займає більшу частину часу.
Остін Д

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

Принаймні, у цьому тривіальному прикладі, спробу твердження не мав помітного впливу на продуктивність. Сміливо вимірюйте більш складні.

Взагалі кажучи, я рекомендую не турбуватися про вартість продуктивності мовних конструкцій, поки у вас не з’являться докази фактичної проблеми з продуктивністю у вашому коді. Або як висловився Дональд Кнут : "передчасна оптимізація - корінь усього зла".


4
в той час, як спроба / ні спроба дуже схожа на більшості JVM, мікробензик дуже страшний.
bestsss

2
досить мало рівнів: ви маєте на увазі, що результати обчислюються в межах 1н? Скомпільований код повністю видалить цикл try / catch AND (підсумовуючи число від 1 до n - це тривіальна сума арифметичної прогресії). Навіть якщо код містить спробу / нарешті компілятор може довести, нічого не можна кидати туди. Анотаційний код має лише 2 сайти для викликів, і він буде клонований та вкладений. Випадків більше, просто шукайте статті про microbenchmark, і ви вирішили написати мікро-позначку завжди перевіряйте створену збірку.
bestsss

3
Повідомлені часи - за повторення циклу. Оскільки вимірювання буде використано лише в тому випадку, якщо у нього загальний час минув> 0,1 секунди (або 2 мільярди ітерацій, чого тут не було), я вважаю ваше твердження про те, що цикл було вилучено в повному обсязі важко повірити - тому що якщо цикл було знято, на що потрібно 0,1 секунди?
meriton

... і справді, відповідно -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, і цикл, і додавання до них присутні в створеному нашому коді. І ні, абстрактні методи не накреслені, оскільки їх абонент не просто вчасно складений (мабуть, тому, що він не викликається достатньо разів).
meriton

Як написати правильний мікро-тест в Java: stackoverflow.com/questions/504103 / ...
Vadzim

47

try/ catchможе мати певний вплив на продуктивність. Це тому, що заважає JVM робити деякі оптимізації. Джошуа Блох в "Ефективній Java" сказав наступне:

• Розміщення коду всередині блоку пробного захоплення гальмує певні оптимізації, які в іншому випадку можуть бути виконані сучасними програмами JVM.


25
"це заважає JVM робити деякі оптимізації" ...? Чи можете ви взагалі детальніше розробити?
Кракен

5
@ Код Кракена всередині пробних блоків (як правило? Завжди?) Не може бути повторно замовлений кодом поза блоками спробу, як один із прикладів.
Паташу

3
Зауважимо, що питання полягало у тому, чи "це дорого", а не чи "чи має це вплив на продуктивність?
mikołak

3
додав уривок з Ефективної Java , і це, звичайно, біблія Java; якщо немає посилання, уривок нічого не говорить. Практично будь-який код у Java знаходиться в межах спроби / нарешті на якомусь рівні.
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 залишити блок, інакше він потрапить у catch/finallyкадр.
bestsss

@bestsss - Навіть якщо генерується GOTO (що не є заданою), вартість цього є мізерною, і це далеко не "маркер" для межі блоку - GOTO може бути створений для багатьох конструкцій.
Гарячі лизи

Я ніколи не згадував вартість, однак немає створених байт-кодів - це помилкова заява. Це все. Насправді в байт-коді немає блоків, фрейми не дорівнюють блокам.
bestsss

GOTO не буде, якщо спроба потрапить прямо в остаточний час, і є інші сценарії, де GOTO не буде. Справа в тому, що в порядку байтів "ввести спробу" / "вийти спробувати" немає нічого.
Гарячі лизи

Не буде ГОТО, якщо спроба потрапить прямо в остаточний результат - Неправдиво! finallyу байт-коді немає , це - try/catch(Throwable any){...; throw any;}І у нього є оператор вилову без кадру, і його можна передати, що ОБОВ'ЯЗКОВО бути визначеним (ненулевим) тощо. Чому ви намагаєтесь сперечатися з теми, ви можете перевірити хоча б якийсь байт-код? Поточне керівництво для імпл. нарешті, це копіювання блоків і уникнення переходу в розділ 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, тому вони пропускають оптимізацію, що не може гарантувати безпеку, але сама реалізація досить легка.


4
Ці макроси C, які ви знайшли для try / catch, не еквівалентні Java або реалізації C #, які випромінюють 0 інструкцій часу виконання.
Паташу

Реалізація java надто обширна, щоб включити її в повному обсязі, це спрощена реалізація з метою розуміння основної ідеї того, як можна реалізувати винятки. Сказати, що він видає 0 інструкцій часу виконання, вводить в оману. Наприклад, простий classcastexception розширює runtimeexception, який розширює виняток, який розширюється для передачі, що включає: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... Це як би сказати, що перемикачем у C є безкоштовно, якщо коли-небудь використовується лише 1 випадок, все ще є невеликий запуск накладних витрат.
технозавр

1
@Patashu Усі ці попередньо складені біти все ще повинні завантажуватися при запуску, незалежно від того, чи використовуються вони ніколи. Немає способу дізнатися, чи не буде винятком пам'яті під час запуску в час компіляції - саме тому вони називаються винятками з часу запуску - інакше вони будуть попередженнями / помилками компілятора, тому ні, це не оптимізує все , увесь код для обробки входить до складеного коду та має вартість запуску.
технозавр

2
Я не можу говорити про C. У C # та Java спробуйте реалізувати, додавши метадані, а не код. Коли вводиться блок спробу, нічого не виконується, щоб це вказувати - коли кидається виняток, стек розкручується і метадані перевіряються на обробники цього типу винятків (дорого).
Паташу

1
Так, я реально реалізував інтерпретатор Java & компілятор статичного байт-коду і працював над наступним JITC (для IBM iSeries), і я можу вам сказати, що немає нічого, що "позначає" вхід / вихід діапазону спробу в байт-кодах, а навпаки діапазони визначені в окремій таблиці. Перекладач не робить нічого особливого для діапазону спроб (поки не буде винято виняток). JITC (або статичний компілятор байт-кодів) повинен знати про межі придушення оптимізації, як зазначено раніше.
Гарячі лизи

8

Ще один мікро-орієнтир ( джерело ).

Я створив тест, в якому вимірюю версію коду пробування і коду без спроби лову на основі відсотка винятку. 10% відсотків означає, що 10% тестових випадків мали поділ на нульові випадки. В одній ситуації він обробляється блоком "пробування", в іншій - умовним оператором. Ось моя таблиця результатів:

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)було для мене досить вдосконаленим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.