¿Por qué la cantidad de variables locales utilizadas en un método de código de bytes Java no es la más económica?
jvm javac (5)
Tengo un fragmento de código Java simple:
public static void main(String[] args) {
String testStr = "test";
String rst = testStr + 1 + "a" + "pig" + 2;
System.out.println(rst);
}
Compílelo con el compilador Eclipse Java y examine el bytecode usando AsmTools. Muestra:
Hay tres variables locales en el método. El argumento está en la ranura 0, y el código usa supuestamente las ranuras 1 y 2. Pero creo que 2 variables locales son suficientes: el índice 0 es el argumento de todos modos, y el código solo necesita una variable más.
Para ver si mi idea es correcta, edité el bytecode textual, reduje el número de variables locales a 2 y ajusté algunas instrucciones relacionadas:
¡Lo recompilé con AsmTools y funciona bien!
Entonces, ¿por qué Javac o el compilador de Eclipse no hacen este tipo de optimización para usar las variables locales mínimas?
Bueno, acabas de hacer una falsa dependencia entre lo que solían ser dos locales completamente separados. Esto significaría que el compilador JIT necesita ser más complejo / lento para desentrañar el cambio y volver al código de bytes original de todos modos, o estaría restringido en el tipo de optimizaciones que puede hacer.
Tenga en cuenta que el compilador de Java se ejecuta una vez, en su máquina de desarrollo (o compilación). Es el compilador JIT que conoce el hardware (y el software) en el que se está ejecutando. El compilador de Java necesita crear un código simple y directo que el JIT pueda procesar y optimizar (o, en algunos casos, interpretar). Hay muy pocas razones para optimizar excesivamente el bytecode en sí mismo: puede reducir algunos bytes del tamaño ejecutable, pero ¿por qué molestarse, especialmente si el resultado sería un código menos eficiente de la CPU o un tiempo de compilación JIT más largo?
No tengo el entorno para hacer una prueba real en este momento, pero estoy bastante seguro de que el JIT producirá el mismo código ejecutable a partir de los dos códigos de byte (ciertamente lo hace en .NET, que en muchos aspectos es similar).
Hay varias razones. En primer lugar, no es necesario para el rendimiento. La JVM ya optimiza las cosas en tiempo de ejecución, por lo que no tiene sentido agregar complejidad redundante al compilador.
Sin embargo, otra razón importante que nadie ha mencionado aquí es la depuración. Hacer que el código de bytes esté lo más cerca posible de la fuente original hace que sea mucho más fácil depurar.
La promesa de Java es que el mismo código puede ejecutarse en múltiples sistemas. Java podría optimizar su código de bytes desde el principio. Pero prefiere esperar hasta que se conozcan todos los hechos.
- Hardware: el mismo código de byte podría ejecutarse en un raspberry pi o en un servidor Unix multinúcleo con 64 GB.
- Uso: Algunas funciones casi nunca se llaman y otras se llaman varias veces por segundo.
- Flexibilidad: en el futuro, el bytecode podría ejecutarse en una JVM diferente, que ofrece nuevas optimizaciones. (JDK x?)
Entonces, al posponer las decisiones, el código de bytes se puede reestructurar y afinar aún mejor, con respecto a todas estas variables.
Conclusión: no cambie el nombre / mueva / elimine variables solo para hacer que el código sea más rápido.
Por qué es tan importante el uso:
Java realiza un seguimiento de qué métodos se llaman con mayor frecuencia y qué flujos se siguen con mayor frecuencia a través del código.
Entonces, una posible optimización es la "incorporación de métodos", lo que significa que métodos completos no solo se reestructuran sino que se fusionan. Una vez que combine los métodos, puede trabajar en bloques de código más grandes y optimizar aún mejor. En realidad, puede eliminar variables aún más, reutilizándolas a lo largo de flujos completos.
Simplemente porque Java obtiene rendimiento del compilador justo a tiempo.
Lo que haces en el código fuente de Java, e incluso lo que aparece en los archivos de clase, no es lo que permite el rendimiento en tiempo de ejecución. Por supuesto, no debes descuidar esa parte, sino solo en el sentido de evitar hacer "estúpido" aquí.
Significado: el jvm decide en tiempo de ejecución si vale la pena traducir un método al código de máquina (¡altamente optimizado!). Si el jvm decide que "no vale la pena optimizarlo", ¿por qué hacer que javac sea más complejo y lento al tener mucha optimización allí? Además: ¡cuanto más simple y básico sea el código de bytes entrantes, más fácil será para el JIT analizar y mejorar esa entrada!
También hay un problema con la verificación de bytecode. Usted sabe que cada variable en Jave debe definirse antes de que pueda usarse. Si combina la variable x y la variable y juntas, y el orden es "definir x, usar x, usar y", que debería ser detectado como un error por el verificador de bytecode, pero después de fusionar las dos variables ya no sería detectable.
Como optimización, es mejor dejarlo al compilador justo a tiempo, que puede decidir qué variables quiere compartir espacio.