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.
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 8La 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
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
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
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ónEntonces, 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
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.