Статьи

Бросать исключения — медленно и безобразно

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

Это было типичное монолитное приложение Java EE, отвечающее за выставление счетов. Точный код лучше забыть, но я напоминаю, что разработчик нашел действительно умный способ управления потоком бизнес-процессов.

Часть потока была обычным бесконечным беспорядком «если-то-еще», но что делало вещи более «захватывающими», так это то, что некоторые случайные элементы этих проверок были погружены в пользовательскую механику обработки исключений java.lang.RuntimeException . Таким образом, вы можете иметь код, подобный следующему:

01
02
03
04
05
06
07
08
09
10
11
12
13
if (invoice.overdue()) {
  if (invoice.getCustomer().isKeyCustomer())
    throw new InvoiceOverdueException(InvoiceOverdueException.KEY_CUSTOMER);
  else
    doSomething();
}
else {
  if (invoice.getDueAmount() > BIG_AMOUNT)
    if (invoice.getCustomer().isKeyCustomer())
      //be silent
    else
      throw new InvoiceExceededException(invoice.getDueAmount());
}

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

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

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

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

Я нашел виновника только из-за того, что это был часто используемый блок кода в конкретном алгоритме обхода графа, поэтому было крайне важно выжать из него последние миллисекунды. Удаление обработки исключений немедленно сделало этот блок кода более чем в 100 раз быстрее.

Это может вызвать у вас интерес — почему медленная обработка исключений? Медленная часть связана с созданием исключения. Или — если быть более точным — любой подкласс java.lang.Throwable .

Если вы помните, все конструкторы вызывают вызов конструктора по умолчанию суперкласса через вызов super (). Если вы не укажете этот вызов самостоятельно, компилятор будет любезен добавить его в сам байт-код. В любом случае, просматривая исходный код java.lang.Throwable , вы видите ответ, глядя в лицо:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public Throwable() {
        fillInStackTrace();
    }
 
    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }
 
    private native Throwable fillInStackTrace(int dummy);

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class NewExceptionTest {
 
  @GenerateMicroBenchmark
  public Object baseline() {
    return new Object();
  }
 
  @GenerateMicroBenchmark
  public Object exceptional() {
    return new RuntimeException();
  }
}
1
2
3
Benchmark                          Mode Thr    Cnt  Sec         Mean   Mean error    Units
j.NewExceptionTest.baseline        avgt   1      5    5        3.275        0.029  nsec/op
j.NewExceptionTest.exceptional     avgt   1      5    5     1329.266        8.675  nsec/op

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

Ссылка: Бросать исключения — медленно и безобразно от нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .