Como a concatenação de strings é implementada no Java 9?

111

Conforme escrito em JEP 280: Indify String Concatenation :

Altere a Stringsequência de bytecode de concatenação estática gerada por javacpara usar invokedynamicchamadas para funções de biblioteca JDK. Isso permitirá otimizações futuras de Stringconcatenação sem a necessidade de alterações adicionais no bytecode emitido por javac.

Aqui eu quero entender o que é o uso de invokedynamicchamadas e como a concatenação de bytecode é diferente invokedynamic?

Mohit Tyagi
fonte
11
Eu escrevi sobre isso um tempo atrás - se isso ajudar, vou condensar em uma resposta.
Nicolai,
10
Além disso, dê uma olhada neste vídeo que explica muito bem o ponto do novo mecanismo de concatenação de strings: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov
3
@ZhekaKozlov Eu gostaria de poder votar em seu comentário duas vezes, links que vêm de pessoas que realmente estão implementando tudo isso são os melhores.
Eugene,
2
@ Nicolai: Isso seria ótimo e seria uma resposta melhor do que qualquer outra aqui (incluindo a minha). Quaisquer partes da minha resposta que você queira incorporar quando o fizer, sinta-se à vontade - se você incluir (basicamente) a coisa toda como parte da resposta mais ampla, eu simplesmente excluirei a minha. Como alternativa, se você quiser apenas adicionar à minha resposta, pois é bastante visível, criei um wiki da comunidade.
TJ Crowder

Respostas:

95

O método "antigo" produz um monte de StringBuilderoperações 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);
    }
}

Se compilarmos isso com JDK 8 ou anterior e, em seguida, usarmos javap -c Examplepara ver o bytecode, veremos algo assim:

public class Example {
  public Exemplo ();
    Código:
       0: aload_0
       1: invokespecial # 1 // Método java / lang / Object. "<init>" :() V
       4: retorno

  public static void main (java.lang.String []);
    Código:
       0: novo # 2 // classe 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 // String -
      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 // String -
      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: retorno
}

Como você pode ver, ele cria um StringBuildere usa append. Isso é bastante ineficiente, pois a capacidade padrão do buffer embutido StringBuilderé de apenas 16 caracteres e não há como o compilador saber alocar mais com antecedência, então ele acaba tendo que realocar. É também um monte de chamadas de método. (Observe que a JVM às vezes pode detectar e reescrever esses padrões de chamadas para torná-los mais eficientes.)

Vejamos o que o Java 9 gera:

public class Example {
  public Exemplo ();
    Código:
       0: aload_0
       1: invokespecial # 1 // Método java / lang / Object. "<init>" :() V
       4: retorno

  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: retorno
}

Oh meu, mas isso é mais curto. :-) Ele faz uma única chamada para makeConcatWithConstantsfrom StringConcatFactory, que diz isso em seu Javadoc:

Métodos para facilitar a criação de métodos de concatenação de String, que podem ser usados ​​para concatenar eficientemente um número conhecido de argumentos de tipos conhecidos, possivelmente após adaptação de tipo e avaliação parcial de argumentos. Esses métodos são normalmente usados ​​como métodos de bootstrap para invokedynamicsites de chamada, para oferecer suporte ao recurso de concatenação de string da linguagem de programação Java.

T.J. multidão
fonte
41
Isso me lembra de uma resposta que escrevi quase 6 anos atrás: stackoverflow.com/a/7586780/330057 - Alguém perguntou se eles deveriam fazer um StringBuilder ou apenas usar o antigo +=no loop for. Eu disse a eles que depende, mas não vamos esquecer que eles podem encontrar uma maneira melhor de concatenar em cadeia em algum momento. A linha principal é realmente a penúltima linha:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa 01 de
3
@corsiKa: LOL! Mas uau, demorou muito para chegar lá (não quero dizer seis anos, quero dizer 22 ou mais ... :-))
TJ Crowder
1
@supercat: Pelo que entendi, existem algumas razões, não menos importante, que criar um array varargs para passar para um método em um caminho de desempenho crítico não é o ideal. Além disso, o uso invokedynamicpermite que diferentes estratégias de concatenação sejam escolhidas em tempo de execução e vinculadas na primeira chamada, sem a sobrecarga de uma chamada de método e tabela de despacho em cada chamada; mais no artigo de nicolai aqui e no JEP .
TJ Crowder
1
@supercat: E então há o fato de que não funcionaria bem com não-Strings, pois eles teriam que ser pré-convertidos para String em vez de serem convertidos no resultado final; mais ineficiência. Poderia fazer isso Object, mas então você teria que encaixotar todos os primitivos ... (que Nicolai cobre em seu excelente artigo, aliás.)
TJ Crowder
2
@supercat Eu estava me referindo ao String.concat(String)método já existente cuja implementação é criar o array da string resultante no local. A vantagem torna-se discutível quando temos que invocar toString()objetos arbitrários. Da mesma forma, ao chamar um método que aceita um array, o chamador deve criar e preencher o array, o que reduz o benefício geral. Mas agora, é irrelevante, pois a nova solução é basicamente o que você estava considerando, exceto que não tem sobrecarga de boxing, não precisa de criação de array e o back-end pode gerar manipuladores otimizados para cenários específicos.
Holger
20

Antes de entrar nos detalhes da invokedynamicimplementação usada para otimização da concatenação de String, na minha opinião, é necessário obter algumas informações sobre o que é invokedynamic e como faço para usá-lo?

A invokedynamic instrução simplifica e potencialmente melhora as implementações de compiladores e sistemas de tempo de execução para linguagens dinâmicas na JVM . Ele faz isso permitindo que o implementador da linguagem defina o comportamento de vinculação personalizado com a invokedynamicinstrução que envolve as etapas a seguir.


Provavelmente, eu tentaria levá-lo através delas com as mudanças que foram trazidas para a implementação da otimização de concatenação de String.

  • Definindo o método de bootstrap: - Com Java9, os métodos de bootstrap para invokedynamicsites de chamadas, para suportar a concatenação de strings principalmente makeConcate makeConcatWithConstantsforam introduzidos com a StringConcatFactoryimplementação.

    O uso de invokedynamic fornece uma alternativa para selecionar uma estratégia de tradução até o tempo de execução. A estratégia de tradução usada StringConcatFactoryé semelhante à LambdaMetafactoryintroduzida na versão anterior do java. Além disso, um dos objetivos do JEP mencionado na pergunta é estender ainda mais essas estratégias.

  • Especificando Entradas de Pool Constantes : - Estes são os argumentos estáticos adicionais para a invokedynamicinstrução diferente de (1) MethodHandles.Lookupobjeto que é uma fábrica para criar identificadores de método no contexto da invokedynamicinstrução, (2) um Stringobjeto, o nome do método mencionado na chamada dinâmica site e (3) o MethodTypeobjeto, a assinatura do tipo resolvido do site de chamada dinâmica.

    Já estão vinculados durante a vinculação do código. No tempo de execução, o método de bootstrap é executado e vinculado ao código real fazendo a concatenação. Ele reconfigura a invokedynamicchamada com uma invokestaticchamada apropriada . Isso carrega a string constante do pool constante, os argumentos estáticos do método bootstrap são aproveitados para passar essas e outras constantes diretamente para a chamada do método bootstrap.

  • Usando a Instrução invokedynamic : - Oferece as facilidades para uma ligação lenta, fornecendo os meios para inicializar o destino da chamada uma vez, durante a invocação inicial. A ideia concreta para otimização aqui é substituir toda a StringBuilder.appenddança por uma simples invokedynamicchamada para java.lang.invoke.StringConcatFactory, que aceitará os valores na necessidade de concatenação.

A proposta do Indify String Concatenation afirma com um exemplo o benchmarking do aplicativo com Java9, onde um método semelhante ao compartilhado por @TJ Crowder é compilado e a diferença no bytecode é bastante visível entre as várias implementações.

Naman
fonte
17

Vou adicionar um pouco de detalhes aqui. A parte principal a se entender é que a forma como a concatenação de strings é feita é uma decisão de tempo de execução, não mais uma decisão de tempo de compilação . Portanto, ele pode mudar, o que significa que você compilou seu código uma vez com o java-9 e pode alterar a implementação subjacente da maneira que quiser, sem a necessidade de recompilar.

E o segundo ponto é que no momento existem 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
}

Você pode escolher qualquer um deles através de um parâmetro: -Djava.lang.invoke.stringConcat. Observe que StringBuilderainda é uma opção.

Eugene
fonte