tag - jstl change variable value
¿Es costosa la declaración de variables? (12)
Mientras codificaba en C, me encontré con la siguiente situación.
int function ()
{
if (!somecondition) return false;
internalStructure *str1;
internalStructure *str2;
char *dataPointer;
float xyz;
/* do something here with the above local variables */
}
Teniendo en cuenta que la declaración
if
en el código anterior puede regresar de la función, puedo declarar las variables en dos lugares.
-
Antes de la declaración
if
. -
Después de la declaración
if
.
Como programador, pensaría mantener la declaración de variable después de
if
Statement.
¿El lugar de la declaración cuesta algo? ¿O hay alguna otra razón para preferir un camino sobre el otro?
Cada vez que asigna variables locales en un ámbito C (como las funciones), no tienen un código de inicialización predeterminado (como los constructores C ++).
Y dado que no están asignados dinámicamente (son solo punteros no inicializados), no es necesario invocar funciones adicionales (y potencialmente costosas) (por ejemplo,
malloc
) para prepararlas / asignarlas.
Debido a la forma en que funciona la
stack
, asignar una variable de pila simplemente significa disminuir el puntero de la pila (es decir, aumentar el tamaño de la pila, porque en la mayoría de las arquitecturas, crece hacia abajo) para dejar espacio para ella.
Desde la perspectiva de la CPU, esto significa ejecutar una instrucción SUB simple:
SUB rsp, 4
(en caso de que su variable sea de 4 bytes, como un entero normal de 32 bits).
Además, cuando declara múltiples variables, su compilador es lo suficientemente inteligente como para agruparlas en una gran instrucción
SUB rsp, XX
, donde
XX
es el tamaño total de las variables locales de un ámbito.
En teoria.
En la práctica, sucede algo un poco diferente.
En situaciones como estas, considero que GCC Explorer es una herramienta invaluable cuando se trata de descubrir (con gran facilidad) lo que sucede "bajo el capó" del compilador.
Así que echemos un vistazo a lo que sucede cuando realmente escribe una función como esta: enlace del explorador GCC .
Código C
int function(int a, int b) {
int x, y, z, t;
if(a == 2) { return 15; }
x = 1;
y = 2;
z = 3;
t = 4;
return x + y + z + t + a + b;
}
Asamblea resultante
function(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
cmp DWORD PTR [rbp-20], 2
jne .L2
mov eax, 15
jmp .L3
.L2:
-- snip --
.L3:
pop rbp
ret
Como resultado, GCC es aún más inteligente que eso.
Ni siquiera realiza la instrucción SUB para asignar las variables locales.
Simplemente (internamente) supone que el espacio está "ocupado", pero no agrega ninguna instrucción para actualizar el puntero de la pila (por ejemplo,
SUB rsp, XX
).
Esto significa que el puntero de la pila no se mantiene actualizado, pero dado que en este caso no se realizan más instrucciones
PUSH
(y no se realizan búsquedas rsp relativas) después de utilizar el espacio de la pila, no hay problema.
Aquí hay un ejemplo donde no se declaran variables adicionales: http://goo.gl/3TV4hE
Código C
int function(int a, int b) {
if(a == 2) { return 15; }
return a + b;
}
Asamblea resultante
function(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 2
jne .L2
mov eax, 15
jmp .L3
.L2:
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
.L3:
pop rbp
ret
Si observa el código antes del retorno prematuro (
jmp .L3
, que salta al código de limpieza y retorno), no se invocan instrucciones adicionales para "preparar" las variables de la pila.
La única diferencia es que los parámetros de función ayb, que se almacenan en los registros
edi
y
esi
, se cargan en la pila en una dirección más alta que en el primer ejemplo (
[rbp-4]
y
[rbp - 8]
).
Esto se debe a que no se ha "asignado" espacio adicional para las variables locales como en el primer ejemplo.
Entonces, como puede ver, la única "sobrecarga" para agregar esas variables locales es un cambio en un término de resta (es decir, ni siquiera agregar una operación de resta adicional).
Entonces, en su caso, prácticamente no hay costo por declarar simplemente las variables de la pila.
En última instancia, depende del compilador, pero generalmente todos los locales se asignan al comienzo de la función.
Sin embargo, el costo de asignar variables locales es muy pequeño ya que se colocan en la pila (o se colocan en un registro después de la optimización).
En C, creo que todas las declaraciones de variables se aplican como si estuvieran en la parte superior de la declaración de función; si los declaras en un bloque, creo que es solo una cuestión de alcance (no creo que sea lo mismo en C ++). El compilador realizará todas las optimizaciones en las variables, y algunas incluso pueden desaparecer efectivamente en el código de la máquina en optimizaciones más altas. Luego, el compilador decidirá cuánto espacio necesitan las variables, y luego, durante la ejecución, creará un espacio conocido como la pila donde viven las variables.
Cuando se llama a una función, todas las variables que utiliza su función se colocan en la pila, junto con información sobre la función que se llama (es decir, la dirección de retorno, los parámetros, etc.). No importa dónde se declaró la variable, solo si se declaró, y se asignará a la pila, independientemente.
Declarar variables no es "costoso" per se; Si es lo suficientemente fácil como para no ser utilizado como una variable, el compilador probablemente lo eliminará como una variable.
Mira esto:
Wikipedia sobre pilas de llamadas , algún otro lugar en la pila
Por supuesto, todo esto depende de la implementación y del sistema.
En C99 y versiones posteriores (o con la extensión conforme común a C89), puede mezclar declaraciones y declaraciones.
Al igual que en versiones anteriores (solo más a medida que los compiladores se volvieron más inteligentes y más agresivos), el compilador decide cómo asignar registros y apilar, o hacer cualquier cantidad de otras optimizaciones que se ajusten a la regla como si fuera.
Eso significa que en cuanto al rendimiento, no se espera ninguna diferencia.
De todos modos, esa no fue la razón por la que se permitió:
Fue para restringir el alcance y, por lo tanto, reducir el contexto que un humano debe tener en cuenta al interpretar y verificar su código.
La mejor práctica es adaptar un enfoque perezoso , es decir, declararlos solo cuando realmente los necesite;) (y no antes). Resulta en el siguiente beneficio:
El código es más legible si esas variables se declaran tan cerca del lugar de uso como sea posible.
Mantenga la declaración lo más cerca posible del lugar donde se usa.
Idealmente dentro de bloques anidados.
Entonces, en este caso, no tendría sentido declarar las variables por encima de la instrucción
if
.
Prefiero mantener la condición de "salida anticipada" en la parte superior de la función, además de documentar por qué lo estamos haciendo. Si lo ponemos después de un montón de declaraciones de variables, alguien que no esté familiarizado con el código podría perderlo fácilmente, a menos que sepa que tiene que buscarlo.
Documentar la condición de "salida anticipada" por sí sola no siempre es suficiente, también es mejor dejarlo claro en el código. Poner la condición de salida temprana en la parte superior también hace que sea más fácil mantener el documento sincronizado con el código, por ejemplo, si luego decidimos eliminar la condición de salida temprana o agregar más condiciones de este tipo.
Sí, puede costar claridad. Si hay un caso en el que la función no debe hacer nada en absoluto en alguna condición (como al encontrar el falso global, en su caso), entonces colocar el cheque en la parte superior, donde lo muestra arriba, seguramente es más fácil de entender: algo que es esencial al depurar y / o documentar.
Si declara variables después de la declaración if y regresa de la función inmediatamente, el compilador no compromete memoria en la pila.
Si realmente importaba, la única forma de evitar la asignación de las variables es:
int function_unchecked();
int function ()
{
if (!someGlobalValue) return false;
return function_unchecked();
}
int function_unchecked() {
internalStructure *str1;
internalStructure *str2;
char *dataPointer;
float xyz;
/* do something here with the above local variables */
}
Pero en la práctica, creo que no encontrarás ningún beneficio de rendimiento. En todo caso una minúscula sobrecarga.
Por supuesto, si estaba codificando C ++ y algunas de esas variables locales tenían constructores no triviales, probablemente necesitaría colocarlos después de la verificación. Pero incluso entonces no creo que ayude dividir la función.
Si tienes esto
int function ()
{
{
sometype foo;
bool somecondition;
/* do something with foo and compute somecondition */
if (!somecondition) return false;
}
internalStructure *str1;
internalStructure *str2;
char *dataPointer;
float xyz;
/* do something here with the above local variables */
}
entonces el espacio de pila reservado para
foo
y
somecondition
puede obviamente reutilizarse para
str1
, etc., por lo que al declarar después del
if
, puede ahorrar espacio de pila.
Dependiendo de las capacidades de optimización del compilador, el ahorro de espacio en la pila también
puede
tener lugar si aplana la función quitando el par de llaves internas o si declara
str1
etc. antes del
if
;
sin embargo, esto requiere que el compilador / optimizador
note
que los ámbitos no se "superponen" realmente.
Al postular las declaraciones después de
if
facilita este comportamiento incluso sin optimización, sin mencionar la legibilidad mejorada del código.
Haga lo que tenga sentido, pero el estilo de codificación actual recomienda colocar las declaraciones de variables lo más cerca posible de su uso
En realidad, las declaraciones de variables son gratuitas en prácticamente todos los compiladores después de la primera. Esto se debe a que prácticamente todos los procesadores administran su pila con un puntero de pila (y posiblemente un puntero de cuadro). Por ejemplo, considere dos funciones:
int foo() {
int x;
return 5; // aren''t we a silly little function now
}
int bar() {
int x;
int y;
return 5; // still wasting our time...
}
Si tuviera que compilarlos en un compilador moderno (y decirle que no sea inteligente y que optimice mis variables locales no utilizadas), vería esto (ejemplo de ensamblaje x64 ... otros son similares):
foo:
push ebp
mov ebp, esp
sub esp, 8 ; 1. this is the first line which is different between the two
mov eax, 5 ; this is how we return the value
add esp, 8 ; 2. this is the second line which is different between the two
ret
bar:
push ebp
mov ebp, esp
sub esp, 16 ; 1. this is the first line which is different between the two
mov eax, 5 ; this is how we return the value
add esp, 16 ; 2. this is the second line which is different between the two
ret
Nota: ¡ambas funciones tienen el mismo número de códigos de operación!
Esto se debe a que prácticamente todos los compiladores asignarán todo el espacio que necesitan por adelantado (salvo las cosas elegantes como
alloca
que se manejan por separado).
De hecho, en x64, es
obligatorio
que lo hagan de esta manera eficiente.
(Editar: como señaló Forss, el compilador puede optimizar algunas de las variables locales en registros. Más técnicamente, debería argumentar que el primer valor "derramarse" en la pila cuesta 2 códigos de operación, y el resto son gratuitos)
Por las mismas razones, los compiladores recopilarán todas las declaraciones de variables locales y les asignarán espacio por adelantado. C89 requiere que todas las declaraciones sean iniciales porque fue diseñado para ser un compilador de 1 paso. Para que el compilador C89 supiera cuánto espacio asignar, necesitaba conocer todas las variables antes de emitir el resto del código. En lenguajes modernos, como C99 y C ++, se espera que los compiladores sean mucho más inteligentes que en 1972, por lo que esta restricción es más relajada para la conveniencia del desarrollador.
Las prácticas modernas de codificación sugieren colocar las variables cerca de su uso
Esto no tiene nada que ver con los compiladores (que obviamente no podrían importarle de una forma u otra). Se ha encontrado que la mayoría de los programadores humanos leen el código mejor si las variables se colocan cerca de donde se usan. Esta es solo una guía de estilo, así que siéntase libre de estar en desacuerdo con ella, pero existe un consenso notable entre los desarrolladores de que esta es la "forma correcta".
Ahora para algunos casos de esquina:
- Si está utilizando C ++ con constructores, el compilador asignará el espacio por adelantado (ya que es más rápido hacerlo de esa manera y no hace daño). Sin embargo, la variable no se construirá en ese espacio hasta la ubicación correcta en el flujo del código. En algunos casos, esto significa que poner las variables cerca de su uso puede ser incluso más rápido que ponerlas al frente ... el control de flujo podría dirigirnos a la declaración de variables, en cuyo caso ni siquiera es necesario llamar al constructor.
-
alloca
se maneja en una capa por encima de esto. Para aquellos que tienen curiosidad,alloca
implementaciones dealloca
tienden a tener el efecto de mover el puntero de la pila hacia abajo en una cantidad arbitraria. Las funciones que utilizanalloca
son necesarias para realizar un seguimiento de este espacio de una forma u otra, y asegurarse de que el puntero de la pila se reajuste hacia arriba antes de partir. -
Puede haber un caso en el que generalmente necesita 16 bytes de espacio de pila, pero con una condición necesita asignar una matriz local de 50kB.
No importa dónde coloque sus variables en el código, prácticamente todos los compiladores asignarán 50kB + 16B de espacio de pila cada vez que se llama a la función.
Esto rara vez importa, pero en un código obsesivamente recursivo esto podría desbordar la pila.
Debe mover el código que trabaja con la matriz de 50kB a su propia función, o usar
alloca
. - Algunas plataformas (p. Ej., Windows) necesitan una llamada de función especial en el prólogo si asigna más de una página de espacio de pila. Esto no debería cambiar mucho el análisis (en la implementación, es una función de hoja muy rápida que solo introduce 1 palabra por página).