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.
Class.getName()
y en el setUp()
método, no en el cuerpo de la referencia.
this.name
.