2011-07-06 32 views
14

He estado mirando algunas de las colecciones primitivas de java (trove, fastutil, hppc) y he notado un patrón de que las variables de clase a veces se declaran como variables locales final. Por ejemplo:¿Es más rápido acceder a las variables locales finales que las variables de clase en Java?

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    final int[] key = this.key; 
    final int[] value = this.value; 
    for (int i = 0; i < used.length; i++) { 
     if (used[i]) { 
      p.apply(key[i],value[i]); 
     } 
    } 
} 

que he hecho alguna evaluación comparativa, y parece que es un poco más rápido cuando se hace esto, pero ¿por qué es este el caso? Intento entender qué haría Java de manera diferente si las primeras tres líneas de la función estuvieran comentadas.

Nota: Esto parece ser similar a this question, pero eso fue para C++ y no aborda por qué se declaran final.

+1

se puede tratar de buscar en el conjunto de Java generado para ver la diferencia. –

+0

acaba de darse cuenta de que la razón puede estar en el compilador de HotSpot, no en el código de bytes ... –

+0

Publique su código de evaluación comparativa, hay al menos alguna posibilidad de que haya evaluado el método erróneamente y realmente haya probado solo el intérprete y no el compilador :) – Voo

Respuesta

8

La palabra clave final es una pista falsa aquí. La diferencia de rendimiento se debe a que dicen dos cosas diferentes.

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    for (int i = 0; i < used.length; i++) { 
    ... 
    } 
} 

ya es decir, "buscar una matriz booleana, y para cada elemento de la matriz que hacer algo".

Sin final boolean[] used, la función está diciendo ", mientras que el índice es menor que la longitud del valor actual del campo del objeto actual used, buscar el valor actual del campo del objeto actual used y hacer algo con el elemento en el índice i. "

El JIT puede tener un tiempo mucho más fácil para probar invariantes enlazados en bucle para eliminar el exceso de comprobaciones de límite, etc., porque puede determinar mucho más fácilmente qué haría que cambie el valor de used. Incluso ignorando múltiples hilos, si p.apply pudiera cambiar el valor de used entonces el JIT no puede eliminar las verificaciones de límites u otras optimizaciones útiles.

+0

Estoy confundido en cuanto a lo que quieres decir con "final" es una pista falsa. ¿Quiere decir que no es necesariamente más rápido acceder a la variable, pero el compilador JIT puede optimizar el ciclo para eliminar las búsquedas y los controles de rango? – job

+0

"Ignorar varios hilos", para dejar esto en claro: el JIT ** solo ** considera el comportamiento local del hilo. Esto significa que incluso si se usa es público (o hay un método setter) y puede cambiarse por otro hilo, el JIT tiene todo el derecho de ignorar esto. Entonces, el JIT realmente solo tiene que averiguar si apply() cambiará la referencia o no (en la práctica: si puede alinear la llamada (y todas las llamadas secundarias) lo notará, de lo contrario, no tendrá suerte) – Voo

+0

también hay una buena probabilidad de que el comportamiento "más rápido" viene porque alguien escribió una vez más un punto de referencia de java no válido (demasiado fácil de hacer eso y demasiado duro para hacerlo bien) - no debe haber una diferencia de rendimiento en el intérprete pero si aplicable es bastante simple, en realidad no debería haber ninguna diferencia en el código compilado con modernos hotspot – Voo

2

le dice al tiempo de ejecución (jit) que en el contexto de esa llamada al método, esos 3 valores nunca cambiarán, por lo que el tiempo de ejecución no necesita cargar continuamente los valores de la variable miembro. esto puede dar una ligera mejora de velocidad.

por supuesto, a medida que el jit se hace más inteligente y puede resolver estas cosas por sí mismo, estas convenciones se vuelven menos útiles.

nota, no dejé claro que la aceleración es más por el uso de una variable local que la parte final.

+0

¡Oye, estaba escribiendo esto también! :-) Excepto que creo que incluso el compilador puede beneficiarse al saber que el método no está interesado en cambios paralelos en estas referencias. – Szocske

25

Acceder a la variable o parámetro local es una operación de un solo paso: tomar una variable ubicada en el desplazamiento N en la pila. Si función tiene 2 argumentos (simplificada):

  • N = 0 - this
  • N = 1 - primer argumento
  • N = 2 - segundo argumento
  • N = 3 - primera variable local
  • N = 4 - segunda variable local
  • ...

De modo que cuando accede a la variable local, tiene un acceso a la memoria con desplazamiento fijo (se conoce N en el momento de la compilación). Este es el código de bytes para acceder primer argumento método (int):

iload 1 //N = 1 

Sin embargo, cuando se accede a campo, en realidad se está llevando a cabo un paso adicional. En primer lugar usted está leyendo "variable local" this sólo para determinar la dirección del objeto actual. Luego está cargando un campo (getfield) que tiene un desplazamiento fijo desde this. Entonces realiza dos operaciones de memoria en lugar de una (o una extra). Código de bytes:

aload 0 //N = 0: this reference 
getfield total I //int total 

De modo que acceder técnicamente a las variables y parámetros locales es más rápido que los campos de objetos. En la práctica, muchos otros factores pueden afectar el rendimiento (incluidos varios niveles de caché de CPU y optimizaciones de JVM).

final es una historia diferente. Básicamente, es una sugerencia para el compilador/JIT que esta referencia no cambiará, por lo que puede hacer algunas optimizaciones más pesadas. Pero esto es mucho más difícil de rastrear, como regla general, use final siempre que sea posible.

+5

Supongo que esta respuesta (y particularmente su último párrafo) es mejor que la marcada. –

+0

Tengo que preguntarme si parte de la aceleración final podría ser que un JIT inteligente podría reutilizar un puntero antes de que el objeto salga del alcance, y ahorrar en alloc(), además de obtener mejores éxitos de caché para tener una memoria ligeramente más pequeña huella ... – Ajax

+0

Totalmente de acuerdo. La respuesta más útil. – omniyo

1

En los códigos de máquina virtuales generados, las variables locales son entradas en la pila de operandos, mientras que las referencias de campo se deben mover a la pila a través de una instrucción que recupera el valor a través de la referencia del objeto. Me imagino que el JIT puede hacer que las referencias de la pila registren referencias más fácilmente.

+2

No del todo bien. Las variables locales se colocan en el hilo * stack *, no en * operand stack *. varios códigos de operación 'load' /' store' se usan para mover variables locales desde la pila a la pila de operandos y viceversa. Ver [esta imagen] (http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/fig01.gif). –

0

Tales optimizaciones simples ya están incluidos en la JVM en tiempo de ejecución. Si JVM tiene acceso ingenuo a las variables de instancia, nuestras aplicaciones Java serán lentas.

Tal sintonización manual, probablemente vale la pena para las JVM más simples, sin embargo, por ejemplo, Androide.

+0

Dex (androide) código de bytes es probablemente aún más eficiente ... un .DX sin compresión es menor que un .class jar-comprimido, y la única razón para Dalvik sobre java móvil fue el rendimiento (JVM estándar es demasiado bloaty para dispositivos móviles) – Ajax

Cuestiones relacionadas