Concatenación de cadenas o código de bytes de parche

Recientemente leí un artículo sobre la optimización del rendimiento del código Java, en particular, la concatenación de cadenas. La pregunta seguía ahí: por qué cuando se usa StringBuilder en el código debajo del corte, el programa se ejecuta más lentamente que con una simple adición. En este caso, + = durante la compilación se convierte en llamadas StringBuilder.append ().

Inmediatamente tuve el deseo de resolver el 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(); } 

Entonces todo mi razonamiento se redujo al hecho de que esta es una magia inexplicable dentro de la JVM, y dejé de tratar de darme cuenta de lo que estaba sucediendo. Sin embargo, durante la siguiente discusión sobre las diferencias entre plataformas en la velocidad de trabajar con cadenas, mi amigo yegorf1 y yo decidimos averiguar por qué y cómo sucede exactamente esta magia.

Oracle Java SE


upd: se realizaron pruebas en Java 8
La solución obvia es recopilar el código fuente en bytecode y luego mirar su contenido. Entonces lo hicimos. En los comentarios hubo sugerencias de que la aceleración está relacionada con la optimización: las líneas constantes obviamente deberían estar pegadas en el nivel de compilación. Resultó que esto no es así. Aquí hay una parte del bytecode descompilado con 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; 

Puede notar que no se han realizado optimizaciones. Extraño, ¿no es así? Bien, veamos el bytecode de la segunda función.

  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; 

Aquí de nuevo, no hay optimizaciones? Además, echemos un vistazo a las instrucciones en 8, 14 y 15 bytes. Allí sucede algo extraño: primero, una referencia a un objeto de la clase StringBuilder se carga en la pila, luego se arroja desde la pila y se vuelve a cargar. La solución más simple viene a la 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; 

Desechando instrucciones innecesarias, obtenemos un código que funciona 1.5 veces más rápido que la versión stringAppend, en la que esta optimización ya se ha llevado a cabo. Por lo tanto, la culpa de la "magia" es el compilador de bytecode incompleto, que no puede realizar optimizaciones bastante simples.

Android ART


upd: el código fue construido bajo sdk 28 con las herramientas de compilación
Entonces, resultó que el problema está relacionado con la implementación del compilador de Java en el código de bytes para la pila JVM. Aquí recordamos la existencia de ART, que es parte del Proyecto de Código Abierto de Android . Esta máquina virtual, o más bien, el compilador de código de bytes en código nativo, fue escrita en una demanda de Oracle, lo que nos da todas las razones para creer que las diferencias con la implementación de Oracle son significativas. Además, debido a los detalles de los procesadores ARM, esta máquina virtual es un registro, no una pila.

Echemos un vistazo a Smali (una de las representaciones de bytecode en 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; //... 

En esta variante stringAppendBuilder no hay más problemas con la pila: la máquina es un registro y, en principio, no pueden ocurrir. Sin embargo, esto no interfiere con la existencia de cosas absolutamente mágicas:

 move-result-object v1 

Esta cadena en stringAppend no hace nada: el enlace al objeto StringBuilder que necesitamos ya está en el registro v1. Sería lógico suponer que stringAppend funcionará más lentamente. Esto se confirma experimentalmente: el resultado es similar al resultado de la versión "parcheada" del programa para la pila JVM: StringBuilder funciona casi una vez y media más rápido.

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


All Articles