¿Por qué StringBuilder # append (int) es más rápido en Java 7 que en Java 8?
Mientras investigaba para un pequeño debate w. r. t. usando "" + n
y Integer.toString(int)
para convertir un entero primitivo a una cadena escribí esto JMH microbenchmark:
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
protected int counter;
@GenerateMicroBenchmark
public String integerToString() {
return Integer.toString(this.counter++);
}
@GenerateMicroBenchmark
public String stringBuilder0() {
return new StringBuilder().append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder1() {
return new StringBuilder().append("").append(this.counter++).toString();
}
@GenerateMicroBenchmark
public String stringBuilder2() {
return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
}
@GenerateMicroBenchmark
public String stringFormat() {
return String.format("%d", this.counter++);
}
@Setup(Level.Iteration)
public void prepareIteration() {
this.counter = 0;
}
}
Lo ejecuté con las opciones predeterminadas de JMH con ambas máquinas virtuales Java que existen en mi máquina Linux (Mageia 4 de 64 bits actualizada, CPU Intel i7-3770, 32 GB de RAM). La primera JVM fue la suministrada con Oracle JDK 8u5 64-bit:
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)
Con esta JVM tengo más o menos lo que esperado:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32317.048 698.703 ops/ms
b.IntStr.stringBuilder0 thrpt 20 28129.499 421.520 ops/ms
b.IntStr.stringBuilder1 thrpt 20 28106.692 1117.958 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20066.939 1052.937 ops/ms
b.IntStr.stringFormat thrpt 20 2346.452 37.422 ops/ms
Es decir, usar la clase StringBuilder
es más lento debido a la sobrecarga adicional de crear el objeto StringBuilder
y agregar una cadena vacía. Usar String.format(String, ...)
es aún más lento, por un orden de magnitud más o menos.
El compilador proporcionado por la distribución, por otro lado, se basa en OpenJDK 1.7:
java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
Los resultados aquí fueron interesantes :
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 31249.306 881.125 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39486.857 663.766 ops/ms
b.IntStr.stringBuilder1 thrpt 20 41072.058 484.353 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20513.913 466.130 ops/ms
b.IntStr.stringFormat thrpt 20 2068.471 44.964 ops/ms
¿Por qué StringBuilder.append(int)
aparece mucho más rápido con esta JVM? Mirando el código fuente de la clase StringBuilder
no reveló nada particularmente interesante-el método en cuestión es casi idéntico a Integer#toString(int)
. Curiosamente, agregar el resultado de Integer.toString(int)
(el stringBuilder2
microbenchmark) no parece ser más rápido.
¿Es esta discrepancia de rendimiento un problema con el arnés de prueba? ¿O mi JVM OpenJDK contiene optimizaciones que afectarían a este código en particular (anti)-patrón?
EDITAR:
Para una comparación más sencilla, instalé Oracle JDK 1. 7u55:
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)
Los resultados son similares a los de OpenJDK:
Benchmark Mode Samples Mean Mean error Units
b.IntStr.integerToString thrpt 20 32502.493 501.928 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39592.174 428.967 ops/ms
b.IntStr.stringBuilder1 thrpt 20 40978.633 544.236 ops/ms
Parece que este es un problema más general de Java 7 vs Java 8. Tal vez Java 7 tenía optimizaciones de cadenas más agresivas?
EDITAR 2:
Para completar, aquí están las opciones de VM relacionadas con cadenas para ambas JVM:
Para Oracle JDK 8u5:
$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
Para OpenJDK 1.7:
$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
bool UseStringCache = false {product}
La opción UseStringCache
se eliminó en Java 8 sin reemplazo, por lo que duda que hace alguna diferencia. El resto de las opciones parecen tener la misma configuración.
EDITAR 3:
Una comparación lado a lado del código fuente de la AbstractStringBuilder
, StringBuilder
y Integer
clases del archivo src.zip
de no revela nada noteworty. Aparte de una gran cantidad de cambios cosméticos y de documentación, Integer
ahora tiene soporte para enteros sin signo y StringBuilder
ha sido ligeramente refactorizado para compartir más código con StringBuffer
. Ninguno de estos cambios parece afectar la rutas de código utilizadas por StringBuilder#append(int)
, aunque puede que me haya perdido algo.
Una comparación del código ensamblador generado para IntStr#integerToString()
y IntStr#stringBuilder0()
es mucho más interesante. El diseño básico del código generado para IntStr#integerToString()
era similar para ambas JVM, aunque Oracle JDK 8u5 parecía ser más agresivo w.r.t. insertando algunas llamadas dentro del código Integer#toString(int)
. Había una correspondencia clara con el código fuente de Java, incluso para alguien con experiencia mínima en ensamblado.
El código de ensamblaje para IntStr#stringBuilder0()
, sin embargo, era radicalmente diferente. El código generado por Oracle JDK 8u5 estaba una vez más directamente relacionado con el código fuente de Java - pude reconocer fácilmente el mismo diseño. Por el contrario, el código generado por OpenJDK 7 era casi irreconocible para el ojo inexperto (como el mío). La llamada new StringBuilder()
fue aparentemente eliminada, al igual que la creación del array en el constructor StringBuilder
. Además, el complemento desensamblador no fue capaz de proporcionar tantas referencias al código fuente como lo hizo en JDK 8.
Asumo que esto es el resultado de un pase de optimización mucho más agresivo en OpenJDK 7, o más probablemente el resultado de insertar código de bajo nivel escrito a mano para ciertas operaciones StringBuilder
. No estoy seguro de por qué esta optimización no ocurre en mi implementación de JVM 8 o por qué las mismas optimizaciones no se implementaron para Integer#toString(int)
en JVM 7. Supongo que alguien familiarizado con las partes relacionadas del código fuente de JRE tendría que responder a estas preguntas...
2 answers
TL; DR: Los efectos secundarios en append
aparentemente rompen las optimizaciones de StringConcat.
Muy buen análisis en la pregunta original y actualizaciones!
Para completar, a continuación se presentan algunos pasos que faltan:
-
Ver a través de la
-XX:+PrintInlining
para ambos 7u55 y 8u5. En 7u55, verás algo como esto:@ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 java.lang.StringBuilder::<init> (7 bytes) inline (hot) @ 18 java.lang.StringBuilder::append (8 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot)
...y en 8u5:
@ 16 org.sample.IntStr::inlineSideEffect (25 bytes) force inline by CompilerOracle @ 4 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) @ 18 java.lang.StringBuilder::append (8 bytes) inline (hot) @ 2 java.lang.AbstractStringBuilder::append (62 bytes) already compiled into a big method @ 21 java.lang.StringBuilder::toString (17 bytes) inline (hot) @ 13 java.lang.String::<init> (62 bytes) inline (hot) @ 1 java.lang.Object::<init> (1 bytes) inline (hot) @ 55 java.util.Arrays::copyOfRange (63 bytes) inline (hot) @ 54 java.lang.Math::min (11 bytes) (intrinsic) @ 57 java.lang.System::arraycopy (0 bytes) (intrinsic)
Es posible que note que la versión 7u55 es menos profunda, y parece que no se llama a nada después de los métodos
StringBuilder
this esta es una buena indicación de que las optimizaciones de cadenas están en efecto. De hecho, si ejecuta 7u55 con-XX:-OptimizeStringConcat
, las subcalls reaparecerán y el rendimiento se reducirá a niveles 8u5. OK, así que tenemos que averiguar por qué 8u5 no hace la misma optimización. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot para que "StringBuilder" averigüe dónde maneja la VM la optimización de StringConcat; esto le llevará a
src/share/vm/opto/stringopts.cpp
-
hg log src/share/vm/opto/stringopts.cpp
para averiguar los últimos cambios allí. Uno de los candidatos sería:changeset: 5493:90abdd727e64 user: iveresov date: Wed Oct 16 11:13:15 2013 -0700 summary: 8009303: Tiered: incorrect results in VM tests stringconcat...
Busque los hilos de revisión en las listas de correo de OpenJDK (bastante fácil de Google para el resumen de conjuntos de cambios): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html
Spot " String concat optimization optimization colapsa el patrón [...] en una sola asignación de una cadena y formando el resultado directamente. Todas las deopts posibles que pueden ocurrir en el código optimizado reinician este patrón desde el principio (a partir de la asignación de StringBuffer). Eso significa que todo el patrón debe liberarme de efectos secundarios." Eureka?
-
Escriba el punto de referencia contrastante:
@Fork(5) @Warmup(iterations = 5) @Measurement(iterations = 5) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) public class IntStr { private int counter; @GenerateMicroBenchmark public String inlineSideEffect() { return new StringBuilder().append(counter++).toString(); } @GenerateMicroBenchmark public String spliceSideEffect() { int cnt = counter++; return new StringBuilder().append(cnt).toString(); } }
-
Mídalo en JDK 7u55, viendo el mismo rendimiento para el lado en línea / empalmado efectos:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 65.460 1.747 ns/op o.s.IntStr.spliceSideEffect avgt 25 64.414 1.323 ns/op
-
Mídalo en JDK 8u5, viendo la degradación del rendimiento con el efecto de línea:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.inlineSideEffect avgt 25 84.953 2.274 ns/op o.s.IntStr.spliceSideEffect avgt 25 65.386 1.194 ns/op
Enviar el informe de error ( https://bugs.openjdk.java.net/browse/JDK-8043677 ) para discutir este comportamiento con los chicos de VM. La justificación de la solución original es sólida como una roca, es interesante, sin embargo, si podemos / debemos recuperar esta optimización en algunos casos triviales como estos.
???
BENEFICIO.
Y sí, debería publicar los resultados para el benchmark que mueve el incremento de la cadena StringBuilder
, haciéndolo antes de toda la cadena. También, cambiado a tiempo promedio, y ns / op. Esto es JDK 7u55:
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.805 1.093 ns/op o.s.IntStr.stringBuilder0 avgt 25 128.284 6.797 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.524 3.116 ns/op o.s.IntStr.stringBuilder2 avgt 25 254.384 9.204 ns/op o.s.IntStr.stringFormat avgt 25 2302.501 103.032 ns/op
Y esto es 8u5: {[19]]}
Benchmark Mode Samples Mean Mean error Units o.s.IntStr.integerToString avgt 25 153.032 3.295 ns/op o.s.IntStr.stringBuilder0 avgt 25 127.796 1.158 ns/op o.s.IntStr.stringBuilder1 avgt 25 131.585 1.137 ns/op o.s.IntStr.stringBuilder2 avgt 25 250.980 2.773 ns/op o.s.IntStr.stringFormat avgt 25 2123.706 25.105 ns/op
stringFormat
en realidad es un poco más rápido en 8u5, y todas las otras pruebas son las mismas. Esto solidifica la hipótesis el efecto secundario rotura de cadenas de SB en el mayor culpable en la pregunta original.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-21 20:37:23
Creo que esto tiene que ver con la bandera CompileThreshold
que controla cuando el código de bytes es compilado en código máquina por JIT.
El Oracle JDK tiene un recuento predeterminado de 10.000 como documento en http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html .
Donde OpenJDK no pude encontrar un último documento en esta bandera; pero algunos hilos de correo sugieren un umbral mucho más bajo: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html
También, intente activar / desactivar las banderas JDK de Oracle como -XX:+UseCompressedStrings
y -XX:+OptimizeStringConcat
. Sin embargo, no estoy seguro de si esos indicadores están activados por defecto en OpenJDK. ¿Podría alguien sugerir?
Una experiencia que se puede hacer, es en primer lugar ejecutar el programa por un montón de veces, por ejemplo, 30.000 bucles, hacer un Sistema.gc () y luego tratar de ver el rendimiento. Creo que darían lo mismo.
Y Yo suponga que su configuración de GC también es la misma. De lo contrario, está asignando muchos objetos y el GC podría ser la mayor parte de su tiempo de ejecución.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-20 10:36:16