Код может быть скомпилирован с утверждениями в нем и может быть активирован/деактивирован при необходимости.
Но если я разворачиваю приложение с утверждениями в нем, и они отключены, каков штраф, связанный с термином, и игнорируется?
Код может быть скомпилирован с утверждениями в нем и может быть активирован/деактивирован при необходимости.
Но если я разворачиваю приложение с утверждениями в нем, и они отключены, каков штраф, связанный с термином, и игнорируется?
Вопреки общепринятой мудрости, утверждения оказывают влияние на производительность выполнения. В большинстве случаев это воздействие может быть небольшим, но в определенных обстоятельствах оно может быть большим. Некоторые из механизмов, которые позволяют замедлить работу во время выполнения, являются довольно "плавными" и предсказуемыми (и обычно небольшими), но последний способ, описанный ниже (отказ встроенного), является сложным, поскольку это самая большая потенциальная проблема (у вас может быть регрессия по порядку величины), и она не является гладкой 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, если метод компилируется. Конечно, даже в этом случае вы по-прежнему платите некоторые расходы:
Точки (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
и перевернув статус утверждения по умолчанию (но, как указано выше, вы можете добиться такого же эффекта в общем виде со свойствами).
Очень мало. Я считаю, что они удаляются во время загрузки классов.
Ближайшая вещь, которая у меня есть для доказательства: Утвердить спецификацию утверждения в спецификации Java Langauge. Кажется, это сформулировано так, что утверждения assert могут обрабатываться во время загрузки класса.
Отключение утверждений устраняет их штраф за исполнение. однажды инвалидов, они по существу эквивалентно пустым утверждениям в семантика и производительность