example java performance java-7 java-8 jmh

example - stringbuilder java



¿Por qué StringBuilder#append(int) es más rápido en Java 7 que en Java 8? (2)

Mientras investigaba un pequeño debate wrt usando "" + n Integer.toString(int) para convertir una primitiva entera en una cadena, escribí esta 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 (actualizado Mageia 4 de 64 bits, CPU Intel i7-3770, 32 GB de RAM). La primera JVM fue la suministrada con Oracle JDK 8u5 de 64 bits:

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 obtuve casi lo que esperaba:

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, el uso de la clase StringBuilder es más lento debido a la sobrecarga adicional de crear el objeto StringBuilder y anexar una cadena vacía. Usar String.format(String, ...) es aún más lento, en 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? Observar 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 OpenJDK JVM contiene optimizaciones que afectarían a este código (anti) -pattern en particular?

EDITAR:

Para una comparación más directa, 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. ¿Quizás Java 7 tenía optimizaciones de cadenas más agresivas?

EDICION 2 :

Para completar, estas son las opciones de VM relacionadas con cadenas para estas dos 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 dudo que haga la diferencia. El resto de las opciones parecen tener la misma configuración.

EDIT 3:

Una comparación lado a lado del código fuente de las clases AbstractStringBuilder , StringBuilder e Integer del archivo src.zip de no revela nada. Además de una gran cantidad de cambios cosméticos y de documentación, Integer ahora tiene algo de 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 las rutas de código utilizadas por StringBuilder#append(int) , aunque es posible que haya omitido algo.

Una comparación del código de ensamblado generado para IntStr#integerToString() e IntStr#stringBuilder0() es mucho más interesante. El diseño básico del código generado para IntStr#integerToString() fue similar para ambas JVM, aunque Oracle JDK 8u5 parecía ser más agresivo al insertar algunas llamadas dentro del código Integer#toString(int) . Hubo una correspondencia clara con el código fuente de Java, incluso para alguien con una experiencia de ensamblaje mínima.

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; podía reconocer fácilmente el mismo diseño. Por el contrario, el código generado por OpenJDK 7 era casi irreconocible para el ojo no entrenado (como el mío). La new StringBuilder() llamada new StringBuilder() aparentemente se eliminó, al igual que la creación de la matriz 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.

Supongo que esto es el resultado de una pasada de optimización mucho más agresiva en OpenJDK 7, o más probablemente el resultado de insertar código de bajo nivel escrito a mano para ciertas operaciones de StringBuilder . No estoy seguro de por qué esta optimización no ocurre en mi implementación de JVM 8 o por qué no se implementaron las mismas optimizaciones 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 estas preguntas...


Creo que esto tiene que ver con el indicador CompileThreshold que controla cuando JIT compila el código de bytes en código máquina.

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 el ú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

Además, intente activar / desactivar los indicadores de Oracle JDK como -XX:+UseCompressedStrings y -XX:+OptimizeStringConcat . No estoy seguro si esas banderas están activadas por defecto en OpenJDK. ¿Podría alguien sugerirme?

Un experimento que puede hacer es ejecutar el programa varias veces, por ejemplo, 30,000 bucles, hacer un System.gc () y luego tratar de observar el rendimiento. Creo que rendirían lo mismo.

Y asumo que la configuración de tu GC es la misma también. De lo contrario, está asignando una gran cantidad de objetos y el GC podría ser la parte más importante de su tiempo de ejecución.


TL; DR: los efectos secundarios en el apéndice aparentemente rompen las optimizaciones de StringConcat.

Muy buen análisis en la pregunta original y actualizaciones!

Para completar, a continuación hay algunos pasos que faltan:

  • Consulte a través de -XX:+PrintInlining para 7u55 y 8u5. En 7u55, verá 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)

    Puede observar que la versión 7u55 es menos profunda, y parece que no se llama a nada después de los métodos StringBuilder : esta es una buena indicación de que las optimizaciones de cadena están en efecto. De hecho, si ejecuta 7u55 con -XX:-OptimizeStringConcat , las llamadas secundarias reaparecerán y el rendimiento bajará a niveles de 8u5.

  • OK, entonces tenemos que descubrir por qué 8u5 no hace la misma optimización. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot para "StringBuilder" para averiguar dónde maneja VM la optimización de StringConcat; esto te llevará a src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp para descubrir 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 (lo suficientemente fácil para buscar en Google el resumen del conjunto de cambios): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot "Optimización de optimización de concat de String colapsa el patrón en una sola asignación de una cadena y forma el resultado directamente. Todos los posibles deopts 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 el patrón completo debe ser libre de efectos secundarios. "¿Eureka?

  • Escriba la referencia de contraste:

    @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 los efectos secundarios en línea / empalmados:

    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 integrado:

    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

  • Envíe el informe de error ( https://bugs.openjdk.java.net/browse/JDK-8043677 ) para analizar este comportamiento con los chicos de VM. El fundamento de la corrección original es sólido como una roca, pero es interesante si podemos / debemos recuperar esta optimización en algunos casos triviales como estos.

  • ???

  • LUCRO.

Y sí, debería publicar los resultados para el punto de referencia que mueve el incremento de la cadena StringBuilder , haciéndolo antes de toda la cadena. Además, cambió al tiempo promedio y ns / op. Este 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:

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 es en realidad un poco más rápido en 8u5, y todas las demás pruebas son iguales. Esto solidifica la hipótesis de la rotura de los efectos secundarios en las cadenas SB en el principal culpable en la pregunta original.