Concatenação de cadeias ou bytecode de patch

Recentemente, li um artigo sobre como otimizar o desempenho do código Java - em particular, concatenação de strings. A questão permaneceu - por que, ao usar o StringBuilder no código abaixo do corte, o programa é executado mais lentamente do que com uma simples adição. Nesse caso, + = durante a compilação se transforma em chamadas StringBuilder.append ().

Eu imediatamente tive o desejo de resolver o problema.

// ~20 000 000    public String stringAppend() { String s = "foo"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", bar"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; s += ", baz"; s += ", qux"; return s; } // ~7 000 000    public String stringAppendBuilder() { StringBuilder sb = new StringBuilder(); sb.append("foo"); sb.append(", bar"); sb.append(", bar"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); sb.append(", baz"); sb.append(", qux"); return sb.toString(); } 

Então todo o meu raciocínio se resumiu ao fato de que essa é uma magia inexplicável dentro da JVM, e desisti de tentar perceber o que estava acontecendo. No entanto, durante a próxima discussão sobre as diferenças entre plataformas na velocidade de trabalhar com strings, meu amigo yegorf1 decidiu descobrir por que e como exatamente essa mágica acontece.

Oracle Java SE


upd: testes foram realizados no Java 8
A solução óbvia é coletar o código-fonte no bytecode e, em seguida, examinar seu conteúdo. Então nós fizemos. Nos comentários, houve sugestões de que a aceleração está relacionada à otimização - linhas constantes devem obviamente ser coladas no nível da compilação. Descobriu-se que não é assim. Aqui está uma parte do bytecode descompilado com javap:

  public java.lang.String stringAppend(); Code: 0: ldc #2 // String foo 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

Você pode perceber que nenhuma otimização foi feita. Estranho, não é? Ok, vamos ver o bytecode da segunda função.

  public java.lang.String stringAppendBuilder(); Code: 0: new #3 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: pop 15: aload_1 16: ldc #6 // String , bar 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

Aqui, novamente, sem otimizações? Além disso, vamos dar uma olhada nas instruções em 8, 14 e 15 bytes. Algo estranho acontece lá - primeiro, uma referência a um objeto da classe StringBuilder é carregada na pilha, depois é lançada da pilha e recarregada. A solução mais simples vem à mente:

  public java.lang.String stringAppendBuilder(); Code: 0: new #41 // class java/lang/StringBuilder 3: dup 4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 8: aload_1 9: ldc #2 // String foo 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String , bar 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 

Jogando instruções desnecessárias, obtemos um código que funciona 1,5 vezes mais rápido que a versão stringAppend, na qual essa otimização já foi realizada. Portanto, a falha da “mágica” é o compilador de bytecodes incompleto, que não pode executar otimizações bastante simples.

Android ART


upd: o código foi criado no sdk 28 com o release buildtools
Portanto, verificou-se que o problema está relacionado à implementação do compilador Java no bytecode para a pilha JVM. Aqui, lembramos da existência do ART, que faz parte do Projeto Android Open Source . Essa máquina virtual, ou melhor, o compilador de bytecode em código nativo, foi escrita em um processo da Oracle, o que nos dá todos os motivos para acreditar que as diferenças em relação à implementação do Oracle são significativas. Além disso, devido às especificidades dos processadores ARM, essa máquina virtual é um registro, não uma pilha.

Vamos dar uma olhada no Smali (uma das representações de bytecode no ART):

 # virtual methods .method public stringAppend()Ljava/lang/String; .registers 4 .prologue .line 6 const-string/jumbo v0, "foo" .line 7 .local v0, "s":Ljava/lang/String; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 const-string/jumbo v2, ", bar" invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 //... .method public stringAppendBuilder()Ljava/lang/String; .registers 3 .prologue .line 13 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V .line 14 .local v0, "sb":Ljava/lang/StringBuilder; const-string/jumbo v1, "foo" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; .line 15 const-string/jumbo v1, ", bar" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; //... 

Nesta variante stringAppendBuilder, não há mais problemas com a pilha - a máquina é um registrador e, em princípio, não pode ocorrer. No entanto, isso não interfere na existência de coisas absolutamente mágicas:

 move-result-object v1 

Essa string em stringAppend não faz nada - o link para o objeto StringBuilder de que precisamos já está no registro v1. Seria lógico supor que stringAppend funcione mais lentamente. Isso é confirmado experimentalmente - o resultado é semelhante ao resultado da versão "corrigida" do programa para a pilha JVM: StringBuilder funciona quase uma vez e meia mais rápido.

Source: https://habr.com/ru/post/pt416479/


All Articles