Velocidad de acceso variable Java local vs instancia

Entonces mi pregunta es acerca de la velocidad de acceso variable en Java. Hoy, en mi “CS” (si se puede llamar así), el docente presentó un ejemplo similar al siguiente de una lista:

public class ListExample { private Node head; private Node tail; private class Node { /* ... */ } public void append(T content) { if (!isEmpty()) { Node dummy = new Node(content); head = dummy; tail = dummy; head.setNext(head); // or this dummy.setNext(dummy); } else { /* ... */ } } // more methods // ... } 

Mi pregunta es: ¿Sería la llamada a head.setNext(head) más lenta que dummy.setNext(dummy) ? Incluso si no es notable. Me lo estaba preguntando porque la head es obviamente y la instancia var de la clase y el maniquí es local, ¿así que el acceso local sería más rápido?

Ok, he escrito un micro-benchmark (como lo sugieren @Joni y @MattBall) y aquí están los resultados para 1 x 1000000000 de accesos para cada uno local y una variable de instancia:

 Average time for instance variable access: 5.08E-4 Average time for local variable access: 4.96E-4 

Para 10 x 1000000000 accesos cada uno:

 Average time for instance variable access:4.723E-4 Average time for local variable access:4.631E-4 

Para 100 x 1000000000 accesos cada uno:

 Average time for instance variable access: 5.050300000000002E-4 Average time for local variable access: 5.002400000000001E-4 

Por lo tanto, parece que los accesos variables locales son de hecho más rápidos que los accesos var de instancia (incluso si ambos apuntan al mismo objeto).

Nota: No quería descubrir esto, debido a algo que quería optimizar, era solo de puro interés.

PS Aquí está el código para el micro-benchmark:

 public class AccessBenchmark { private final long N = 1000000000; private static final int M = 1; private LocalClass instanceVar; private class LocalClass { public void someFunc() {} } public double testInstanceVar() { // System.out.println("Running instance variable benchmark:"); instanceVar = new LocalClass(); long start = System.currentTimeMillis(); for (int i = 0; i < N; i++) { instanceVar.someFunc(); } long elapsed = System.currentTimeMillis() - start; double avg = (elapsed * 1000.0) / N; // System.out.println("elapsed time = " + elapsed + "ms"); // System.out.println(avg + " microseconds per execution"); return avg; } public double testLocalVar() { // System.out.println("Running local variable benchmark:"); instanceVar = new LocalClass(); LocalClass localVar = instanceVar; long start = System.currentTimeMillis(); for (int i = 0 ; i < N; i++) { localVar.someFunc(); } long elapsed = System.currentTimeMillis() - start; double avg = (elapsed * 1000.0) / N; // System.out.println("elapsed time = " + elapsed + "ms"); // System.out.println(avg + " microseconds per execution"); return avg; } public static void main(String[] args) { AccessBenchmark bench; double[] avgInstance = new double[M]; double[] avgLocal = new double[M]; for (int i = 0; i < M; i++) { bench = new AccessBenchmark(); avgInstance[i] = bench.testInstanceVar(); avgLocal[i] = bench.testLocalVar(); System.gc(); } double sumInstance = 0.0; for (double d : avgInstance) sumInstance += d; System.out.println("Average time for instance variable access: " + sumInstance / M); double sumLocal = 0.0; for (double d : avgLocal) sumLocal += d; System.out.println("Average time for local variable access: " + sumLocal / M); } } 

En general, un acceso a una variable de instancia (de this objeto) requiere un aload_0 (para cargar this en la parte superior de la stack) seguido de getfield . aload_n referencia a una variable local solo requiere aload_n para extraer el valor de su ubicación asignada en la stack.

Además, getfield debe hacer referencia a la definición de clase para determinar en qué parte de la clase (qué desplazamiento) se almacena el valor. Esto podría ser varias instrucciones de hardware adicionales.

Incluso con un JITC es poco probable que la referencia local (que normalmente sería cero / una operación de hardware) sea más lenta que la referencia de campo de instancia (que tendría que ser al menos una operación, tal vez 2-3).

(No es que esto importe tanto, la velocidad de ambos es bastante buena, y la diferencia solo podría volverse significativa en circunstancias muy extrañas).

Al igual que en los comentarios, no creo que haya diferencia en el tiempo empleado. Creo que lo que podría estar refiriéndose se ejemplifica mejor en la base de código Java SE. Por ejemplo, en java.lang.String :

 public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) { //some code you can check out char[] val = value; while (i < n) { dst[j++] = (byte)val[i++]; /* avoid getfield opcode */ } } 

En el código anterior, el value es una variable de instancia y dado que había un ciclo while donde se iba a acceder a los elementos individuales de value , lo trajeron del montón a la stack ( variable local ) optimizando así.

También puede consultar el conocimiento compartido por Jon Skeet, Vivin y algunos otros en esta respuesta .

Desde la perspectiva de la micro architecture, leer una variable local puede ser más barato porque es probable que esté en un registro o al menos en la memoria caché de la CPU. En general, la lectura de una variable de instancia puede causar una costosa falta de memoria caché. En este caso, aunque la variable acaba de escribirse, probablemente estará en la memoria caché. Podría escribir un micro benchmark para ver si hay alguna diferencia.

Creo que usar dummy podría ser como máximo, 1 ciclo más rápido, suponiendo que se dejó en un registro, pero depende de la architecture de CPU específica, y de cómo se ve setNext , y la JVM que estás usando, y es realmente impredecible cómo podría verse el código una vez en su forma final JIT . La JVM podría ver esa cabeza == ficticia, y si es así, el código ejecutado para ambos casos sería idéntico. Este es un caso mucho, demasiado pequeño para preocuparse.

Puedo asegurarle que cualquier ganancia en el rendimiento que se pueda obtener de esto se verá compensada por el dolor de cabeza de ver códigos confusos. Deja que el comstackdor resuelva esto. Admitiré que, en igualdad de condiciones, la variable local es probablemente un poco más rápida, aunque solo sea porque hay menos instrucciones de bytecode involucradas. Sin embargo, ¿quién puede decir que las versiones futuras de la JVM no cambiarán esto?

En resumen, escriba un código que sea fácil de leer primero. Si, después de eso, tiene una preocupación por el rendimiento, perfil.

En caso de duda, observe el código de bytes generado

 public void append(java.lang.Object); Code: 0: new #2; //class ListExample$Node 3: dup 4: aload_0 5: aload_1 6: invokespecial #3; //Method ListExample$Node."":(LListExample;Ljava/lang/Object;)V 9: astore_2 10: aload_0 11: aload_2 12: putfield #4; //Field head:LListExample$Node; 15: aload_0 16: aload_2 17: putfield #5; //Field tail:LListExample$Node; 20: aload_0 21: getfield #4; //Field head:LListExample$Node; 24: aload_0 25: getfield #4; //Field head:LListExample$Node; 28: invokevirtual #6; //Method ListExample$Node.setNext:(LListExample$Node;)V 31: aload_2 32: aload_2 33: invokevirtual #6; //Method ListExample$Node.setNext:(LListExample$Node;)V 36: return } 

O te vas aload seguido de getfield o 2 x aload. Me parece que serían idénticos …