2010-09-16 11 views
12

Supongamos que tengo una función en un solo programa de rosca que tiene este aspecto¿Pueden las variables locales estáticas reducir el tiempo de asignación de memoria?

void f(some arguments){ 
    char buffer[32]; 
    some operations on buffer; 
} 

yf aparece dentro de alguna de bucle que es llamada a menudo, así que me gustaría que sea lo más rápido posible. Me parece que el buffer necesita ser asignado cada vez que se llama f, pero si declaro que es estático, esto no sucederá. ¿Es ese razonamiento correcto? ¿Es eso una aceleración libre? Y solo por ese hecho (que es una velocidad fácil), ¿un compilador de optimización ya hace algo como esto por mí?

Respuesta

8

Para implementaciones que usan una pila para variables locales, a menudo la asignación de tiempos implica avanzar un registro (agregarle un valor), como el registro del Apilador de pila (SP). Este tiempo es muy insignificante, generalmente una instrucción o menos.

Sin embargo, la inicialización de las variables de pila lleva un poco más de tiempo, pero de nuevo, no mucho.Consulte la lista del lenguaje en ensamblador (generado por el compilador o el depurador) para obtener detalles exactos. No hay nada en el estándar sobre la duración o el número de instrucciones requeridas para inicializar las variables.

La asignación de variables locales estáticas generalmente se trata de manera diferente. Un enfoque común es colocar estas variables en la misma área que las variables globales. Por lo general, todas las variables en esta área se inicializan antes de llamar al main(). La asignación en este caso es una cuestión de asignar direcciones a los registros o almacenar la información del área en la memoria. No se pierde mucho tiempo de ejecución aquí.

La asignación dinámica es el caso en el que se graban los ciclos de ejecución. Pero eso no está en el alcance de su pregunta.

10

Incrementar 32 bytes en la pila costará prácticamente nada en casi todos los sistemas. Pero deberías probarlo. Compare una versión estática y una versión local y publique de nuevo.

+0

+1 por sugerir una herramienta de referencia. Usar un generador de perfiles siempre debe ser el primer paso para verificar si algo 'más rápido' o 'más lento' – linuxuser27

+0

Ya sé por el perfil que no importaba en mi aplicación específica, pero me dio curiosidad si alguna vez lo sería. Y ahora veo por qué no debería. –

1

con GCC, noto cierta aceleración:

void f() { 
    char buffer[4096]; 
} 

int main() { 
    int i; 
    for (i = 0; i < 100000000; ++i) { 
     f(); 
    } 
} 

Y el tiempo:

$ time ./a.out 

real 0m0.453s 
user 0m0.450s 
sys 0m0.010s 

búfer cambiar a la electricidad estática:

$ time ./a.out 

real 0m0.352s 
user 0m0.360s 
sys 0m0.000s 
+0

¿Qué? ¿Sin optimizaciones? Eso no es un punto de referencia significativo. Y, por supuesto, con las optimizaciones habilitadas, es probable que se elimine toda la llamada a 'f'. –

+0

Acabo de notar que la pregunta fue etiquetada como C++, así que lo hice de nuevo compilando con g ++ en lugar de gcc. Curiosamente, la versión no estática funcionaba en ~ 0.5 s, mientras que la versión estática aún se ejecutaba en ~ 0.35s – Colin

+0

¡Oh, sí ... optimizaciones! Con el indicador O2, ambas versiones se ejecutan en 0.002s – Colin

1

Dependiendo de qué es exactamente la variable es haciendo y cómo se usa, la velocidad es casi nada a nada. Debido a que (en los sistemas x86) la memoria de la pila se asigna para todas las variables locales al mismo tiempo con un simple func único (sub esp, amount), por lo tanto, tener solo una otra pila var elimina cualquier ganancia. la única excepción a esto es en realidad buffers enormes en cuyo caso un compilador puede pegarse en _chkstk para asignar memoria (pero si su buffer es tan grande, debería volver a evaluar su código). El compilador no puede convertir la memoria de la pila en memoria estática a través de la optimización, ya que no puede suponer que la función se utilizará en un entorno de subproceso único, además de interferir con los constructores de objetos & destructores, etc.

3

La forma en que está escrito ahora , no hay costo para la asignación: los 32 bytes están en la pila. El único trabajo real es que necesitas inicializar a cero.

La estática local no es una buena idea aquí. No será más rápido, y su función ya no podrá usarse desde múltiples hilos, ya que todas las llamadas comparten el mismo buffer. Sin mencionar que la inicialización estática local no garantiza que sea segura para subprocesos.

+1

Normalmente no hay inicialización cero para las variables de pila. – mmmmmmmm

3

Sugeriría que un enfoque más general a este problema es que si tiene una función llamada muchas veces que necesita algunas variables locales, considere envolverla en una clase y hacer que estas variables sean funciones miembro. Considere si era necesario hacer el tamaño dinámico, por lo que en lugar de char buffer[32] tenía std::vector<char> buffer(requiredSize). Esto es más caro que una matriz para inicializar cada vez a través del bucle

class BufferMunger { 
public: 
    BufferMunger() {}; 
    void DoFunction(args); 
private: 
    char buffer[32]; 
}; 

BufferMunger m; 
for (int i=0; i<1000; i++) { 
    m.DoFunction(arg[i]); // only one allocation of buffer 
} 

Hay otra implicación de hacer que el buffer estático, que es que la función está ahora inseguro en una aplicación multiproceso, como dos hilos pueden llamar y sobrescribir los datos en el búfer al mismo tiempo. Por otro lado, es seguro usar un BufferMunger por separado en cada hilo que lo requiera.

+0

+1: Ahora que lo mencionas, recuerdo haber hecho algo así para una clase que implementó una transformada de Fourier. –

1

Si hay variables locales automáticas en la función, el puntero de pila debe ajustarse. El tiempo necesario para el ajuste es constante y no variará en función del número de variables declaradas. Usted podría ahorrar algo de tiempo si su función no tiene variables automáticas locales de ningún tipo.

Si se inicializa una variable estática, habrá un indicador en algún lugar para determinar si la variable ya se ha inicializado. Verificar la bandera tomará un tiempo. En su ejemplo, la variable no se inicializa, por lo que esta parte se puede ignorar.

Se deben evitar las variables estáticas si su función tiene alguna posibilidad de ser llamada recursivamente o de dos hilos diferentes.

3

Tenga en cuenta que las variables de nivel de bloque static en C++ (a diferencia de C) se inicializan en el primer uso. Esto implica que you'll be introducing the cost of an extra runtime check. La rama podría terminar empeorando el rendimiento, no mejor. (Pero en realidad, debe perfilar, como han mencionado otros).

De todos modos, no creo que valga la pena, especialmente porque estaría sacrificando intencionalmente la reentrada.

1

Hará que la función sea mucho más lenta en la mayoría de los casos reales. Esto se debe a que el segmento de datos estáticos no está cerca de la pila y perderá la coherencia de la memoria caché, por lo que obtendrá un error de caché cuando intente acceder a ella. Sin embargo, cuando asigna un carácter normal [32] en la pila, está justo al lado de todos sus otros datos necesarios y cuesta muy poco acceder. Los costos de inicialización de una matriz de caracteres basada en pila no tienen sentido.

Esto está ignorando que la estática tiene muchos otros problemas.

Realmente necesita crear un perfil de su código y ver dónde están las ralentizaciones, porque ningún generador de perfiles le dirá que asignar un buffer de caracteres de tamaño estático es un problema de rendimiento.

2

Si está escribiendo código para una PC, es poco probable que haya una ventaja de velocidad significativa en ambos sentidos. En algunos sistemas integrados, puede ser ventajoso evitar todas las variables locales. En algunos otros sistemas, las variables locales pueden ser más rápidas.

Un ejemplo de lo anterior: en el Z80, el código para configurar el marco de pila para una función con cualquier variable local era bastante largo. Además, el código para acceder a las variables locales se limitaba a utilizar el modo de direccionamiento (IX + d), que solo estaba disponible para las instrucciones de 8 bits. Si X e Y eran ambos ambas variables locales global/estática o, la afirmación "X = Y" podría montar ya sea como:

 
; If both are static or global: 6 bytes; 32 cycles 
    ld HL,(_Y) ; 16 cycles 
    ld (_X),HL ; 16 cycles 
; If both are local: 12 bytes; 56 cycles 
    ld E,(IX+_Y) ; 14 cycles 
    ld D,(IX+_Y+1) ; 14 cycles 
    ld (IX+_X),D ; 14 cycles 
    ld (IX+_X+1),E ; 14 cycles 

Una pena de espacio de código 100% y 75% penalización de tiempo además del código y hora de configurar el marco de pila!

En el procesador ARM, una sola instrucción puede cargar una variable que se encuentra dentro de +/- 2K de un puntero de dirección. Si las variables locales de una función suman 2K o menos, se puede acceder a ellas con una sola instrucción. Las variables globales generalmente requerirán dos o más instrucciones para cargar, dependiendo de dónde estén almacenadas.

12

No, no es una aceleración gratuita.

En primer lugar, la asignación es casi libre, para empezar (ya que consiste simplemente en añadir 32 al puntero de pila), y en segundo lugar, hay al menos dos razones por las que una variable estática podría ser lento

  • pierde localidad de caché. Los datos asignados en la pila ya estarán en el caché de la CPU, por lo que acceder a ellos es extremadamente económico. Los datos estáticos se asignan en un área diferente de la memoria y, por lo tanto, no se almacenan en la memoria caché, por lo que se perderá la memoria caché y tendrá que esperar cientos de ciclos de reloj para que los datos se obtengan de la memoria principal.
  • pierde la seguridad del hilo. Si dos subprocesos ejecutan la función simultáneamente, se bloqueará y se quemará, a menos que se coloque un bloqueo de manera que solo se permita que un subproceso a la vez ejecute esa sección del código. Y eso significaría que perdería el beneficio de tener múltiples núcleos de CPU.

Así que no es una aceleración gratuita. Pero es posible que sea más rápido en su caso (aunque lo dudo). Así que pruébelo, compárelo y vea qué funciona mejor en su situación particular.

+0

Agregaría que también pierde reentrada: no tiene una variable de limpieza cada vez que ingresa a la función por más tiempo (incluso si está integrado, este es siempre el caso ...) –

+0

Las estáticas pueden ser más rápidas porque son a menudo se asignan en el mismo lugar que los globales. Digamos que tiene una variable global y una variable estática tocada por el mismo código; Es probable que esa estática sea local de caché con respecto a la global, por lo que es probable que leer uno también lea el otro en una línea de caché de la CPU.Las funciones recursivas, las grandes matrices no estáticas y, especialmente, ambas cosas al mismo tiempo pueden agotar la pila, en cuyo caso una buena idea es utilizar cuidadosamente la estática (¡así que tu programa no se bloqueará!) –

+0

@JodyLeeBruchon son ¿Estás diciendo que una variable asignada a la pila local * no * será caché local? – jalf

Cuestiones relacionadas