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.
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 8A 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
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
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
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 buildtoolsPortanto, 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
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.