Подтвердить что ты не робот

Усиление производительности Java-утверждений при отключении

Код может быть скомпилирован с утверждениями в нем и может быть активирован/деактивирован при необходимости.

Но если я разворачиваю приложение с утверждениями в нем, и они отключены, каков штраф, связанный с термином, и игнорируется?

4b9b3361

Ответ 1

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

Анализ

Выполнение утверждения

Когда дело доходит до анализа функциональности assert в Java, приятно то, что они не являются чем-то волшебным на уровне байт-кода/JVM. То есть они реализованы в файле .class с использованием стандартной механики Java в (.java файле) времени компиляции, и они не получают специальной обработки JVM 2 но полагаются на обычные оптимизации, которые применяются к любому компилированному коду исполнения.

Позвольте быстро взглянуть на точно, как они реализованы на современном Oracle 8 JDK (но AFAIK он не изменился в значительной степени навсегда).

Возьмем следующий метод с одним утверждением:

public int addAssert(int x, int y) {
    assert x > 0 && y > 0;
    return x + y;
} 

... скомпилируйте этот метод и декомпилируйте байт-код с помощью javap -c foo.bar.Main:

  public int addAssert(int, int);
    Code:
       0: getstatic     #17                 // Field $assertionsDisabled:Z
       3: ifne          22
       6: iload_1
       7: ifle          14
      10: iload_2
      11: ifgt          22
      14: new           #39                 // class java/lang/AssertionError
      17: dup
      18: invokespecial #41                 // Method java/lang/AssertionError."<init>":()V
      21: athrow
      22: iload_1
      23: iload_2
      24: iadd
      25: ireturn

Первые 22 байта байт-кода связаны с утверждением. Вначале он проверяет скрытое статическое поле $assertionsDisabled и перескакивает по всей логике assert, если это правда. В противном случае он просто выполняет две проверки обычным способом и создает и бросает объект AssertionError(), если они не работают.

Итак, нет ничего особенного в поддержке assert на уровне байт-кода - единственный трюк - это поле $assertionsDisabled, которое - с использованием того же вывода javap - мы можем видеть, что static final инициализирован во время инициализации класса

  static final boolean $assertionsDisabled;

  static {};
    Code:
       0: ldc           #1                  // class foo/Scrap
       2: invokevirtual #11                 // Method java/lang/Class.desiredAssertionStatus:()Z
       5: ifne          12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: putstatic     #17                 // Field $assertionsDisabled:Z

Итак, компилятор создал это скрытое поле static final и загружает его на основе общедоступного метода desiredAssertionStatus().

Ничего волшебного. На самом деле, попробуйте сами сделать то же самое, с нашим собственным статическим полем SKIP_CHECKS, которое мы загружаем на основе системного свойства:

public static final boolean SKIP_CHECKS = Boolean.getBoolean("skip.checks");

public int addHomebrew(int x, int y) {
    if (!SKIP_CHECKS) {
        if (!(x > 0 && y > 0)) {
            throw new AssertionError();
        }
    }
    return x + y;
}

Здесь мы просто записываем longhand, что делает утверждение (мы могли бы даже комбинировать операторы if, но мы постараемся максимально приблизить утверждение как можно ближе). Пусть проверьте вывод:

 public int addHomebrew(int, int);
    Code:
       0: getstatic     #18                 // Field SKIP_CHECKS:Z
       3: ifne          22
       6: iload_1
       7: ifle          14
      10: iload_2
      11: ifgt          22
      14: new           #33                 // class java/lang/AssertionError
      17: dup
      18: invokespecial #35                 // Method java/lang/AssertionError."<init>":()V
      21: athrow
      22: iload_1
      23: iload_2
      24: iadd
      25: ireturn

Да, это почти байт за байт, идентичный версии assert.

Затраты на подтверждение

Таким образом, мы можем в значительной степени уменьшить вопрос о том, насколько дорогим является утвердительный вопрос, "насколько дорогим является код, который перескочил с помощью всегда принятой ветки на основе условия static final?". Хорошей новостью является то, что такие ветки, как правило, полностью оптимизируются компилятором C2, если метод компилируется. Конечно, даже в этом случае вы по-прежнему платите некоторые расходы:

  • Файлы классов больше, и для JIT больше кода.
  • До JIT интерпретированная версия, скорее всего, будет работать медленнее.
  • Полный размер функции используется при встраивании решений, поэтому присутствие утверждений влияет на это решение даже при отключении.

Точки (1) и (2) являются прямым следствием того, что утверждение удаляется во время компиляции во время выполнения (JIT), а не во время компиляции Java файла. Это ключевое отличие от утверждений C и С++ (но взамен вы решаете использовать утверждения при каждом запуске двоичного файла, а не компилировать в этом решении).

Точка (3), вероятно, самая критическая и редко упоминается и ее трудно анализировать. Основная идея заключается в том, что JIT использует пороговые значения размера пары при принятии решений о вложениях - один небольшой порог (~ 30 байт), под которым он почти всегда находится в очереди, и еще один более высокий порог (~ 300 байт), по которому он никогда не встраивается. Между порогами, независимо от того, зависит ли он в строках или нет, зависит ли метод от горячего или нет, а также от других эвристик, например, уже ли он был встроен в другое место.

Поскольку пороговые значения основаны на размере байт-кода, использование утверждений может существенно повлиять на эти решения - в приведенном выше примере полностью 22 из 26 байтов в функции были связаны с утверждением. Особенно, когда используется множество небольших методов, легко утверждать, что они нажимают метод на пороги включения. Теперь пороговые значения - это всего лишь эвристика, поэтому возможно, что изменение метода от inline до not-inline может улучшить производительность в некоторых случаях, но в целом вы хотите больше, чем меньше встраивания, поскольку это оптимизация grand-daddy, которая позволяет еще много раз это происходит.

Смягчение

Один из способов решения этой проблемы состоит в том, чтобы переместить большую часть логики assert в специальную функцию следующим образом:

public int addAssertOutOfLine(int x, int y) {
    assertInRange(x,y);
    return x + y;
}

private static void assertInRange(int x, int y) {
    assert x > 0 && y > 0;
}

Скомпилируется для:

  public int addAssertOutOfLine(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: invokestatic  #46                 // Method assertInRange:(II)V
       5: iload_1
       6: iload_2
       7: iadd
       8: ireturn

... и поэтому уменьшил размер этой функции от 26 до 9 байтов, из которых 5 связаны с утверждением. Конечно, отсутствующий байт-код просто перешел к другой функции, но это прекрасно, потому что это будет рассмотрено отдельно при встраивании решений и JIT-компиляции в no-op, когда утверждения будут отключены.

Истинные значения времени компиляции

Наконец, стоит отметить, что вы можете получить C/С++ - например, компиляцию, если хотите. Это утверждения, состояние включения/выключения которых статически скомпилировано в двоичный файл (в javac время). Если вы хотите включить утверждения, вам нужен новый двоичный файл. С другой стороны, этот тип утверждения действительно свободен во время выполнения.

Если мы изменим homebrew SKIP_CHECKS static final, который будет известен во время компиляции, например:

public static final boolean SKIP_CHECKS = true;

тогда addHomebrew скомпилируется до:

  public int addHomebrew(int, int);
Code:
   0: iload_1
   1: iload_2
   2: iadd
   3: ireturn

То есть, нет следа, оставшегося от утверждения. В этом случае мы можем действительно сказать, что существует нулевая стоимость исполнения. Вы можете сделать это более эффективным в рамках проекта, имея один класс StaticAssert, который обертывает переменную SKIP_CHECKS, и вы можете использовать этот существующий сахар assert для создания 1-строчной версии:

public int addHomebrew2(int x, int y) {
    assert SKIP_CHECKS || (x > 0 && y > 0);
    return x + y;
}

Опять же, это компиляция в javac время до байт-кода без следа assert. Вам придется иметь дело с предупреждением IDE о мертвом коде, хотя (по крайней мере, в затмении).


1 Таким образом, я имею в виду, что эта проблема может иметь нулевой эффект, а затем после небольшого безобидного изменения окружающего кода она может внезапно иметь большой эффект. В основном различные уровни штрафа сильно квантуются из-за бинарного эффекта решений "встроить или не встраивать".

2 По крайней мере, для всей важной части компиляции/выполнения кода, связанного с утверждением, во время выполнения. Конечно, в JVM имеется небольшая поддержка, позволяющая принять аргумент командной строки -ea и перевернув статус утверждения по умолчанию (но, как указано выше, вы можете добиться такого же эффекта в общем виде со свойствами).

Ответ 2

Очень мало. Я считаю, что они удаляются во время загрузки классов.

Ближайшая вещь, которая у меня есть для доказательства: Утвердить спецификацию утверждения в спецификации Java Langauge. Кажется, это сформулировано так, что утверждения assert могут обрабатываться во время загрузки класса.

Ответ 3

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

Источник