Перетаскивание производительности утверждений Java при отключении - PullRequest
25 голосов
/ 07 января 2011

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

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

Ответы [ 3 ]

31 голосов
/ 01 декабря 2016

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

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

 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, , если метод компилируется. Конечно, даже в этом случае вы все равно оплачиваете некоторые расходы:

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

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

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

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

Смягчение

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

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 связаны с assert. Конечно, отсутствующий байт-код только что переместился в другую функцию, но это нормально, потому что он будет учитываться отдельно при принятии решений и JIT-компиляции в no-op, когда утверждения отключены.

Истинные утверждения времени компиляции

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

Если мы изменим домашний напиток 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 для создания однострочной версии:

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

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


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

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

2 голосов
/ 07 января 2011

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

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

1 голос
/ 07 января 2011

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

Источник

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...