¿Cómo se implementa la concatenación de cadenas en Java 9?

111

Como está escrito en JEP 280: Indique la concatenación de cadenas :

Cambie la Stringsecuencia de código de bytes de concatenación estática generada por javacpara usar invokedynamicllamadas a las funciones de la biblioteca JDK. Esto permitirá futuras optimizaciones de Stringconcatenación sin requerir más cambios en el código de bytes emitido por javac.

Aquí quiero entender cuál es el uso de las invokedynamicllamadas y en qué se diferencia la concatenación de códigos de bytes invokedynamic.

Mohit Tyagi
fuente
11
Yo escribí sobre eso hace un tiempo - si eso ayuda, voy a condensarlo en una respuesta.
Nicolai
10
Además, eche un vistazo a este video que explica muy bien el punto del nuevo mecanismo de concatenación de cadenas: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov
3
@ZhekaKozlov Me gustaría poder votar su comentario dos veces, los enlaces que provienen de personas que realmente implementan todo esto son los mejores.
Eugene
2
@Nicolai: Eso sería genial, y sería una mejor respuesta que cualquier otra aquí (incluida la mía). Cualquier parte de mi respuesta que desee incorporar cuando lo haga, siéntase libre: si incluye (básicamente) todo como parte de la respuesta más amplia, simplemente eliminaré la mía. Alternativamente, si solo desea agregar algo a mi respuesta, ya que es bastante visible, lo he convertido en una wiki comunitaria.
TJ Crowder

Respuestas:

95

La forma "antigua" genera un montón de StringBuilderoperaciones orientadas. Considere este programa:

public class Example {
    public static void main(String[] args)
    {
        String result = args[0] + "-" + args[1] + "-" + args[2];
        System.out.println(result);
    }
}

Si compilamos eso con JDK 8 o anterior y luego usamos javap -c Examplepara ver el código de bytes, vemos algo como esto:

Ejemplo de clase pública {
  Ejemplo público ();
    Código:
       0: aload_0
       1: invocarespecial # 1 // Método java / lang / Object. "<init>" :() V
       4: regreso

  public static void main (java.lang.String []);
    Código:
       0: nuevo # 2 // clase java / lang / StringBuilder
       3: dup
       4: invokespecial # 3 // Método java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual # 4 // Método java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // Cadena -
      15: invokevirtual # 4 // Método java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual # 4 // Método java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // Cadena -
      26: invokevirtual # 4 // Método java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: aaload
      32: invokevirtual # 4 // Método java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Método java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Campo java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // Método java / io / PrintStream.println: (Ljava / lang / String;) V
      46: regreso
}

Como puede ver, crea StringBuildery usa append. Esto es bastante ineficiente ya que la capacidad predeterminada del búfer incorporado StringBuilderes de solo 16 caracteres, y el compilador no tiene forma de saber que debe asignar más por adelantado, por lo que termina teniendo que reasignar. También es un montón de llamadas a métodos. (Sin embargo, tenga en cuenta que la JVM a veces puede detectar y reescribir estos patrones de llamadas para hacerlos más eficientes).

Veamos lo que genera Java 9:

Ejemplo de clase pública {
  Ejemplo público ();
    Código:
       0: aload_0
       1: invocarespecial # 1 // Método java / lang / Object. "<init>" :() V
       4: regreso

  public static void main (java.lang.String []);
    Código:
       0: aload_0
       1: iconst_0
       2: aaload
       3: aload_0
       4: iconst_1
       5: aaload
       6: aload_0
       7: iconst_2
       8: aaload
       9: invokedynamic # 2, 0 // InvokeDynamic # 0: makeConcatWithConstants: (Ljava / lang / String; Ljava / lang / String; Ljava / lang / String;) Ljava / lang / String;
      14: astore_1
      15: getstatic # 3 // Campo java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Método java / io / PrintStream.println: (Ljava / lang / String;) V
      22: regreso
}

Oh Dios, pero eso es más corto. :-) Hace una sola llamada a makeConcatWithConstantsfrom StringConcatFactory, que dice esto en su Javadoc:

Métodos para facilitar la creación de métodos de concatenación de cadenas, que se pueden utilizar para concatenar eficazmente un número conocido de argumentos de tipos conocidos, posiblemente después de la adaptación de tipos y la evaluación parcial de argumentos. Estos métodos se utilizan normalmente como métodos de arranque para invokedynamicsitios de llamadas, para admitir la función de concatenación de cadenas del lenguaje de programación Java.

T.J. Crowder
fuente
41
Esto me recuerda una respuesta que escribí hace casi 6 años hasta el día: stackoverflow.com/a/7586780/330057 - Alguien preguntó si deberían hacer un StringBuilder o simplemente usar simplemente antiguo +=en su bucle for. Les dije que depende, pero no olvidemos que podrían encontrar una mejor manera de encadenar concat en algún momento en el futuro. La línea clave es realmente la penúltima línea:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa
3
@corsiKa: ¡LOL! Pero vaya, me tomó mucho tiempo llegar allí (no me refiero a seis años, me refiero a 22 más o menos ... :-))
TJ Crowder
1
@supercat: Según tengo entendido, hay un par de razones, entre las que se incluye el hecho de que crear una matriz varargs para pasar a un método en una ruta de rendimiento crítico no es ideal. Además, el uso invokedynamicpermite elegir diferentes estrategias de concatenación en tiempo de ejecución y enlazarlas en la primera invocación, sin la sobrecarga de una llamada de método y una tabla de despacho en cada invocación; más en el artículo de nicolai aquí y en la JEP .
TJ Crowder
1
@supercat: Y luego está el hecho de que no funcionaría bien con no Strings, ya que tendrían que preconvertirse a String en lugar de convertirse en el resultado final; más ineficiencia. Podría hacerlo Object, pero luego tendrías que encasillar todos los primitivos ... (que Nicolai cubre en su excelente artículo, por cierto)
TJ Crowder
2
@supercat Me refería al String.concat(String)método ya existente cuya implementación está creando la matriz de la cadena resultante en el lugar. La ventaja se vuelve discutible cuando tenemos que invocar toString()objetos arbitrarios. Del mismo modo, al llamar a un método que acepta una matriz, la persona que llama tiene que crear y completar la matriz, lo que reduce el beneficio general. Pero ahora, es irrelevante, ya que la nueva solución es básicamente lo que estaba considerando, excepto que no tiene una sobrecarga de boxeo, no necesita creación de matrices y el backend puede generar controladores optimizados para escenarios particulares.
Holger
20

Antes de entrar en los detalles de la invokedynamicimplementación utilizada para la optimización de la concatenación de cadenas, en mi opinión, uno debe obtener algunos antecedentes sobre ¿Qué es invokedynamic y cómo lo uso?

La invokedynamic instrucción simplifica y potencialmente mejora las implementaciones de compiladores y sistemas de tiempo de ejecución para lenguajes dinámicos en la JVM . Para ello, permite que el implementador del lenguaje defina un comportamiento de enlace personalizado con la invokedynamicinstrucción que implica los siguientes pasos a continuación.


Probablemente trataría de explicarle estos cambios con los cambios que se trajeron para la implementación de la optimización de la concatenación de cadenas.

  • Definición del método Bootstrap : - Con Java9, los métodos bootstrap para invokedynamicsitios de llamadas, para admitir la concatenación de cadenas principalmentemakeConcat y makeConcatWithConstantsse introdujeron con la StringConcatFactoryimplementación.

    El uso de invokedynamic proporciona una alternativa para seleccionar una estrategia de traducción hasta el tiempo de ejecución. La estrategia de traducción utilizada en StringConcatFactoryes similar a la LambdaMetafactoryintroducida en la versión anterior de Java. Además, uno de los objetivos de la JEP mencionado en la pregunta es ampliar aún más estas estrategias.

  • Especificar entradas de grupo constante : - Estos son los argumentos estáticos adicionales a la invokedynamicinstrucción que no sean (1) MethodHandles.Lookupobjeto que es una fábrica para crear identificadores de métodos en el contexto de la invokedynamicinstrucción, (2) un Stringobjeto, el nombre del método mencionado en la llamada dinámica sitio y (3) el MethodTypeobjeto, la firma de tipo resuelto del sitio de llamada dinámica.

    Ya están vinculados durante la vinculación del código. En tiempo de ejecución, el método bootstrap se ejecuta y enlaza en el código real haciendo la concatenación. Reescribe la invokedynamicllamada con una invokestaticllamada apropiada . Esto carga la cadena constante del grupo constante, los argumentos estáticos del método bootstrap se aprovechan para pasar estas y otras constantes directamente a la llamada al método bootstrap.

  • Uso de la instrucción dinámica invocada : - Esto ofrece las facilidades para un enlace perezoso, proporcionando los medios para arrancar el destino de la llamada una vez, durante la invocación inicial. La idea concreta para la optimización aquí es reemplazar todo el StringBuilder.appendbaile con una simple invokedynamicllamada a java.lang.invoke.StringConcatFactory, que aceptará los valores en la necesidad de concatenación.

La propuesta de Indify String Concatenation establece con un ejemplo la evaluación comparativa de la aplicación con Java9 donde un método similar al compartido por @TJ Crowder y la diferencia en el es bastante visible entre las diferentes implementaciones.

Naman
fuente
17

Agregaré algunos detalles aquí. La parte principal es que la forma en que se realiza la concatenación de cadenas es una decisión en tiempo de ejecución, ya no en tiempo de compilación . Por lo tanto, puede cambiar, lo que significa que ha compilado su código una vez contra java-9 y puede cambiar la implementación subyacente como le plazca, sin la necesidad de volver a compilar.

Y el segundo punto es que de momento existen 6 possible strategies for concatenation of String:

 private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

Usted puede elegir cualquiera de ellos a través de un parámetro: -Djava.lang.invoke.stringConcat. Tenga en cuenta que StringBuildersigue siendo una opción.

Eugenio
fuente