Recientemente me he encontrado con un problema relacionado con la concatenación de cadenas. Este punto de referencia lo resume:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {
@Benchmark
public String slow(Data data) {
final Class<? extends Data> clazz = data.clazz;
return "class " + clazz.getName();
}
@Benchmark
public String fast(Data data) {
final Class<? extends Data> clazz = data.clazz;
final String clazzName = clazz.getName();
return "class " + clazzName;
}
@State(Scope.Thread)
public static class Data {
final Class<? extends Data> clazz = getClass();
@Setup
public void setup() {
//explicitly load name via native method Class.getName0()
clazz.getName();
}
}
}
En JDK 1.8.0_222 (OpenJDK 64-Bit Server VM, 25.222-b10) obtuve los siguientes resultados:
Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 22,253 ± 0,962 ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate avgt 25 9824,603 ± 400,088 MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm avgt 25 240,000 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space avgt 25 9824,162 ± 397,745 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm avgt 25 239,994 ± 0,522 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space avgt 25 0,040 ± 0,011 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm avgt 25 0,001 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.count avgt 25 3798,000 counts
BrokenConcatenationBenchmark.fast:·gc.time avgt 25 2241,000 ms
BrokenConcatenationBenchmark.slow avgt 25 54,316 ± 1,340 ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate avgt 25 8435,703 ± 198,587 MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm avgt 25 504,000 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space avgt 25 8434,983 ± 198,966 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm avgt 25 503,958 ± 1,000 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space avgt 25 0,127 ± 0,011 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm avgt 25 0,008 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.count avgt 25 3789,000 counts
BrokenConcatenationBenchmark.slow:·gc.time avgt 25 2245,000 ms
Esto parece un problema similar al JDK-8043677 , donde una expresión que tiene efectos secundarios rompe la optimización de la nueva StringBuilder.append().append().toString()
cadena. Pero el código en Class.getName()
sí mismo no parece tener ningún efecto secundario:
private transient String name;
public String getName() {
String name = this.name;
if (name == null) {
this.name = name = this.getName0();
}
return name;
}
private native String getName0();
Lo único sospechoso aquí es una llamada al método nativo, que de hecho ocurre solo una vez y su resultado se almacena en caché en el campo de la clase. En mi punto de referencia, lo he almacenado en caché explícitamente en el método de configuración.
Esperaba que el predictor de rama descubriera que en cada invocación de referencia el valor real de this.name nunca es nulo y optimiza toda la expresión.
Sin embargo, mientras que para el BrokenConcatenationBenchmark.fast()
tengo esto:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes) force inline by CompileCommand
@ 6 java.lang.Class::getName (18 bytes) inline (hot)
@ 14 java.lang.Class::initClassName (0 bytes) native method
@ 14 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 19 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 23 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 26 java.lang.StringBuilder::toString (35 bytes) inline (hot)
es decir, el compilador puede en línea todo, porque BrokenConcatenationBenchmark.slow()
es diferente:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes) force inline by CompilerOracle
@ 9 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 14 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 18 java.lang.Class::getName (21 bytes) inline (hot)
@ 11 java.lang.Class::getName0 (0 bytes) native method
@ 21 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 24 java.lang.StringBuilder::toString (17 bytes) inline (hot)
Entonces, la pregunta es si este es el comportamiento apropiado de la JVM o el error del compilador.
Estoy haciendo la pregunta porque algunos de los proyectos todavía usan Java 8 y si no se solucionará en ninguna de las actualizaciones de lanzamiento, entonces para mí es razonable levantar llamadas Class.getName()
manualmente desde puntos calientes.
PD En los últimos JDK (11, 13, 14-eap) el problema no se reproduce.
fuente
this.name
.Class.getName()
y en elsetUp()
método, no en el cuerpo de la referencia.Respuestas:
HotSpot JVM recopila estadísticas de ejecución por código de bytes. Si se ejecuta el mismo código en diferentes contextos, el perfil de resultados agregará estadísticas de todos los contextos. Este efecto se conoce como contaminación de perfil .
Class.getName()
obviamente se llama no solo desde su código de referencia. Antes de que JIT comience a compilar el punto de referencia, ya sabe que la siguiente condiciónClass.getName()
se cumplió varias veces:Al menos, suficientes veces para tratar esta rama estadísticamente importante. Por lo tanto, JIT no excluyó esta rama de la compilación y, por lo tanto, no pudo optimizar el concat de cadenas debido a un posible efecto secundario.
Esto ni siquiera necesita ser una llamada a un método nativo. Solo una asignación de campo regular también se considera un efecto secundario.
Aquí hay un ejemplo de cómo la contaminación del perfil puede dañar más optimizaciones.
Esta es básicamente la versión modificada de su punto de referencia que simula la contaminación del
getName()
perfil. Dependiendo del número degetName()
llamadas preliminares en un objeto nuevo, el rendimiento adicional de la concatenación de cadenas puede diferir dramáticamente:Más ejemplos de contaminación del perfil »
No puedo llamarlo un error o un "comportamiento apropiado". Así es como se implementa la compilación adaptativa dinámica en HotSpot.
fuente
Ligeramente no relacionado, pero desde Java 9 y JEP 280: Indicar Concatenación de cadenas, la concatenación de cadenas ahora se hace con
invokedynamic
y noStringBuilder
. Este artículo muestra las diferencias en el código de bytes entre Java 8 y Java 9.Si el punto de referencia que se vuelve a ejecutar en la versión más nueva de Java no muestra el problema, es muy probable que no haya ningún error
javac
porque el compilador ahora usa un nuevo mecanismo. No estoy seguro de si sumergirse en el comportamiento de Java 8 es beneficioso si hay un cambio tan sustancial en las versiones más recientes.fuente
javac
.javac
genera bytecode y no realiza optimizaciones sofisticadas. Ejecuté el mismo punto de referencia-XX:TieredStopAtLevel=1
y recibí esta salida:Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 74,677 ? 2,961 ns/op
BrokenConcatenationBenchmark.slow avgt 25 69,316 ? 1,239 ns/op
así que cuando no optimizamos mucho ambos métodos producen los mismos resultados, el problema se revela solo cuando el código se compila en C2.invokedynamic
solo le dice al tiempo de ejecución que elija cómo hacer la concatenación, y 5 de las 6 estrategias (incluida la predeterminada) aún se usanStringBuilder
.StringConcatFactory.Strategy
enum?