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

18

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

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

    
задан asiar 07.01.2011 в 12:17
источник

3 ответа

16

Вопреки общепринятой мудрости, утверждения оказывают влияние на производительность выполнения. В большинстве случаев это воздействие может быть небольшим, но в определенных обстоятельствах оно может быть большим. Некоторые из механизмов, которые позволяют замедлить работу во время выполнения, являются довольно «плавными» и предсказуемыми (и обычно небольшими), но последний способ, описанный ниже (отказ встроенного), является сложным, поскольку это самая большая потенциальная проблема (у вас может быть регрессия по порядку величины), и она не гладкая 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, if , который скомпилирован. Конечно, даже в этом случае вы по-прежнему платите некоторые расходы:

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

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

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

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

Смягчение

Один из способов решения этой проблемы состоит в том, чтобы переместить большую часть логики 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 / 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 sugar для создания 1-строчной версии:

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

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

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

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

    
ответ дан BeeOnRope 01.12.2016 в 20:44
  • Это, безусловно, лучший ответ, данный здесь. Вы ударяете гвоздь на голову, чтобы утверждать, что он может предотвратить вложение. Ни один из других ответов не был рассмотрен. –  pveentjer 15.01.2017 в 15:43
  • Удивительно, что в этом анализе больше нет голосов ... Возможно, это первоначальное сопротивление концепции, потому что она настолько отличается от того, что считают даже опытные программисты. –  Bill K 01.06.2017 в 19:42
  • @BillK - да, это странно, что за последние шесть лет вопрос имеет относительно мало внимания: важно знать, влияют ли ваши утверждения. Это может быть связано с тем, что вы упоминаете: поскольку assert () свободен в C и C ++ при отключении, возможно, каждый предполагает, что он работает одинаково в Java. Или, возможно, люди предпочитают более богатые, не-утвердительные проверки, такие как предложения, предлагаемые классом Preconditions от guava (в основном я нахожусь в последнем лагере, но я все еще использую утверждения для проверок, которые я действительно хочу использовать в debug / test). В кодовых базах, которые я видел вне JDK, утверждать редко. –  BeeOnRope 01.06.2017 в 20:14
2

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

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

    
ответ дан Gareth Davis 07.01.2011 в 12:20
  • Это то, о чем я никогда не думал. Для удаления при загрузке класса. Ницца. Есть ли ссылка на это, я могу прочитать? –  asiar 07.01.2011 в 12:23
  • Просто смотри сейчас –  Gareth Davis 07.01.2011 в 12:42
  • «Кажется, написано», именно так я его читаю: D. Возможно ли, что это детализация JVM (каждая реализация определяет, как они соблюдают формулировку. Если утверждение отключено, оценка утверждения не имеет никакого эффекта от спецификации)? –  asiar 07.01.2011 в 12:59
  • В ограниченных ситуациях выполнение команды assert выполняется, даже когда утверждения отключены. Поэтому виртуальная машина не может выделять утверждения при загрузке класса, но джиттер не имеет этого ограничения (до тех пор, пока он запускается после инициализации класса). –  Steve McKay 22.04.2015 в 19:42
  • Спецификация однозначно требует, чтобы байт-код был загружен: оператор утверждения, который выполняется до завершения инициализации класса или интерфейса, включен. –  Marko Topolnik 27.01.2016 в 16:03
1
  

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

Источник

    
ответ дан marcog 07.01.2011 в 12:24
  • «После отключения они по сути эквивалентны пустым операторам в семантике и производительности». Как это делается? Как @Gareth Davis упомянул в своем ответе? –  asiar 07.01.2011 в 12:28
  • @asiar Я не могу найти источник для его резервного копирования, но это наиболее вероятное объяснение. –  marcog 07.01.2011 в 12:33
  • Это неверно. Утверждения могут препятствовать встраиванию, поскольку исходный размер метода используется для определения того, может ли что-то быть вложенным. См. Отличный ответ BeeOnRope для лучшего объяснения. –  pveentjer 15.01.2017 в 15:44
  • @asiar - Я перехожу к механике в своем ответе выше. Короче говоря, они не бесплатны, даже если Oracle хочет, чтобы вы поверили, что они есть, если, возможно, вы не префикс этого требования с длинными сериями оговорок. –  BeeOnRope 15.01.2017 в 23:26
  • @pveentjer И ваша отличная статья: pveentjer.blogspot.de/2017/01/... –  Sebastian 30.01.2017 в 06:05