same - Arrastre de rendimiento de las afirmaciones de Java cuando está deshabilitado
java assert library (3)
El código se puede compilar con aserciones y se puede activar / desactivar cuando sea necesario .
Pero si implemento una aplicación con aserciones en ella y esas están deshabilitadas, ¿cuál es la multa que implica que termine estando allí y se ignore?
Al desactivar las aserciones se elimina por completo su penalización de rendimiento. Una vez deshabilitados, son esencialmente equivalentes a declaraciones vacías en semántica y desempeño
Contrariamente a la sabiduría convencional, las afirmaciones tienen un impacto en el tiempo de ejecución y pueden afectar el rendimiento. Es probable que este impacto sea pequeño en la mayoría de los casos, pero podría ser grande en ciertas circunstancias. Algunos de los mecanismos por los cuales las cosas se ralentizan durante el tiempo de ejecución son bastante "suaves" y predecibles (y generalmente pequeñas), pero la última forma analizada a continuación (falla en la línea) es complicada porque es el mayor problema potencial (podría tener una regresión de orden de magnitud) y no es suave 1 .
Análisis
Implementar la aseveración
Cuando se trata de analizar la funcionalidad de assert
en Java, una buena cosa es que no son nada mágico en el nivel de bytecode / JVM. Es decir, se implementan en el archivo .class
utilizando la mecánica estándar de Java en el tiempo de compilación (archivo .java), y no reciben ningún tratamiento especial por parte de la JVM 2 , pero dependen de las optimizaciones habituales que se aplican a cualquier tiempo de ejecución compilado código.
Echemos un vistazo rápido a cómo se implementan en un JDK de Oracle 8 moderno (pero AFAIK no ha cambiado en casi toda la vida).
Tome el siguiente método con una sola afirmación:
public int addAssert(int x, int y) {
assert x > 0 && y > 0;
return x + y;
}
... compile ese método y descompile el bytecode con 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
Los primeros 22 bytes de bytecode están asociados con el aserto. Justo al frente, comprueba el campo $assertionsDisabled
deshabilitadas estáticas ocultas y salta sobre toda la lógica de $assertionsDisabled
si es verdadera. De lo contrario, solo realiza las dos comprobaciones de la forma habitual, y construye y lanza un objeto AssertionError()
si fallan.
Por lo tanto, no hay nada realmente especial en el soporte de $assertionsDisabled
en el nivel de bytecode. El único truco es el campo $assertionsDisabled
, que, utilizando el mismo resultado javap
, podemos ver una static final
inicializada en el momento de inicio de la clase:
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
Así que el compilador ha creado este campo static final
oculto y lo carga basándose en el desiredAssertionStatus()
público desiredAssertionStatus()
.
Así que nada de magia en absoluto. De hecho, intentemos hacer lo mismo con nuestro propio campo estático SKIP_CHECKS
que SKIP_CHECKS
en función de una propiedad del sistema:
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;
}
Aquí simplemente escribimos a largo plazo lo que está haciendo la afirmación (incluso podríamos combinar las declaraciones if, pero trataremos de hacer coincidir la afirmación lo más cerca posible). Vamos a comprobar la salida:
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
Eh, es prácticamente un byte por byte idéntico a la versión Assert.
Afirmar los costos
Entonces, podemos reducir bastante la pregunta "qué tan costosa es una afirmación" a "qué tan costoso es un código saltado por una rama siempre tomada en base a una condición static final
?". La buena noticia es que, por lo general, el compilador C2 optimiza completamente estas ramas, si se compila el método. Por supuesto, incluso en ese caso, usted todavía paga algunos costos:
- Los archivos de clase son más grandes y hay más código para JIT.
- Antes de JIT, la versión interpretada probablemente se ejecutará más lentamente.
- El tamaño completo de la función se utiliza para alinear decisiones, por lo que la presencia de aseveraciones afecta esta decisión incluso cuando está deshabilitada .
Los puntos (1) y (2) son una consecuencia directa de la eliminación de la aserción durante la compilación en tiempo de ejecución (JIT), en lugar de en el tiempo de compilación de archivos java. Esta es una diferencia clave con las aserciones de C y C ++ (pero a cambio usted decide utilizar aserciones en cada lanzamiento del binario, en lugar de compilar esa decisión).
El punto (3) es probablemente el más crítico, y rara vez se menciona y es difícil de analizar. La idea básica es que el JIT utiliza umbrales de tamaño par cuando toma decisiones en línea: un umbral pequeño (~ 30 bytes) bajo el cual casi siempre está en línea, y otro umbral mayor (~ 300 bytes) sobre el que nunca se alinea. Entre los umbrales, el hecho de que esté en línea o no depende de si el método está activo o no, y de otras heurísticas, como si ya se ha introducido en otro lugar.
Dado que los umbrales se basan en el tamaño del código de bytes, el uso de aserciones puede afectar dramáticamente esas decisiones; en el ejemplo anterior, 22 de los 26 bytes en la función estaban relacionados con aserciones. Especialmente cuando se usan muchos métodos pequeños, es fácil para los asaltos empujar un método por encima de los umbrales de alineación. Ahora los umbrales son solo heurísticas, por lo que es posible que el cambio de un método de en línea a no en línea pueda mejorar el rendimiento en algunos casos, pero en general usted desea más en lugar de menos en línea, ya que es una optimización de Grand-Daddy que permite a muchos más una vez. se produce
Mitigación
Un enfoque para solucionar este problema es mover la mayor parte de la lógica de afirmación a una función especial, de la siguiente manera:
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;
}
Esto compila a:
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
... y por lo tanto ha reducido el tamaño de esa función de 26 a 9 bytes, de los cuales 5 están relacionados con aserciones. Por supuesto, el código de bytes que falta se acaba de mover a la otra función, pero eso está bien porque se considerará por separado en las decisiones de alineación y JIT-compila a un no-op cuando las aserciones están deshabilitadas.
Afirmaciones verdaderas en tiempo de compilación
Finalmente, vale la pena señalar que puede obtener afirmaciones en tiempo de compilación como C / C ++, si lo desea. Estas son afirmaciones cuyo estado de activación / desactivación se compila de forma estática en el binario (en tiempo de javac
). Si desea habilitar aserciones, necesita un nuevo binario. Por otro lado, este tipo de aserción es realmente gratuito en el tiempo de ejecución.
Si cambiamos el static final
SKIP_CHECKS homebrew a ser conocido en el momento de la compilación, así:
public static final boolean SKIP_CHECKS = true;
luego addHomebrew
compila a:
public int addHomebrew(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
Es decir, no queda rastro de la afirmación. En este caso, realmente podemos decir que hay cero costo de tiempo de ejecución. Puede hacer que esto sea más viable en un proyecto si tiene una sola clase StaticAssert que envuelva la variable SKIP_CHECKS
, y puede aprovechar esta SKIP_CHECKS
azúcar existente para crear una versión de 1 línea:
public int addHomebrew2(int x, int y) {
assert SKIP_CHECKS || (x > 0 && y > 0);
return x + y;
}
Nuevamente, esto se compila en tiempo de javac a bytecode sin un rastro de la afirmación. Sin embargo, tendrá que lidiar con una advertencia IDE sobre el código muerto (al menos en eclipse).
1 Con esto, quiero decir que este problema puede tener un efecto cero, y luego, después de un pequeño e inocuo cambio en el código circundante, de repente puede tener un gran efecto. Básicamente, los diversos niveles de penalización se cuantifican en gran medida debido al efecto binario de las decisiones "en línea o no en línea".
2 Al menos para la parte más importante de compilar / ejecutar el código relacionado con aserciones en tiempo de ejecución. Por supuesto, hay una pequeña cantidad de soporte en la JVM para aceptar el argumento de la línea de comando -ea
y cambiar el estado de aserción predeterminado (pero como anteriormente, puede lograr el mismo efecto de forma genérica con propiedades).
Muy muy pequeño. Creo que se eliminan durante la carga de la clase.
Lo más cercano que tengo a alguna prueba es: la especificación de la declaración de aserción en la especificación de Java Langauge. Parece estar redactado para que las declaraciones de afirmación puedan procesarse en el tiempo de carga de la clase.