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