7

Muchas veces cuando escribo código, me encuentro usando un valor de una llamada de función particular varias veces. Me di cuenta de que una optimización obvia sería capturar estos valores utilizados repetidamente en las variables. Este (pseudo código):¿Cómo puede un compilador aplicar la eliminación de funciones a funciones impuras?

function add1(foo){ foo + 1; } 
... 
do_something(foo(1)); 
do_something_else(foo(1)); 

se convierte en:

function add1(foo){ foo + 1; } 
... 
bar = foo(1); 
do_something(bar); 
do_something_else(bar); 

Sin embargo, hacer esto hace explícitamente código menos legible en mi experiencia. Supuse que los compiladores no podrían hacer este tipo de optimización si nuestro lenguaje de elección permite que las funciones tengan efectos secundarios.

Recientemente analicé esto, y si entiendo correctamente, esta optimización se puede/se puede hacer para los idiomas donde las funciones deben ser puras. Eso no me sorprende, pero supuestamente esto también se puede hacer para funciones impuras. Con algunas búsquedas rápidas de Google he encontrado estos fragmentos: GCC 4.7 Fortran improvement

Al realizar front-end-optimización, la opción de la función de eliminación de -faggressive permite la eliminación de la función duplicado llamadas, incluso para las funciones impuras.

Compiler Optimization (Wikipedia)

Por ejemplo, en algunas funciones de idiomas no están autorizados a tener efectos secundarios. Por lo tanto, si un programa realiza varias llamadas a la misma función con los mismos argumentos, el compilador puede deducir inmediatamente que el resultado de la función debe computarse solo una vez. En los idiomas en los que se permite que las funciones tengan efectos secundarios, otra estrategia es posible. El optimizador puede determinar qué función no tiene efectos secundarios y restringir dichas optimizaciones a funciones libres de efectos secundarios. Esta optimización solo es posible cuando el optimizador tiene acceso a la función llamada.

Desde mi entender, esto significa que un optimizador puede determinar cuándo una función es o no pura, y realizar esta optimización cuando la función es. Digo esto porque si una función siempre produce la misma salida cuando se le da la misma entrada, y es libre de efectos secundarios, cumpliría ambas condiciones para ser considerada pura.

Estos dos fragmentos me plantean dos preguntas.

  1. ¿Cómo puede un compilador ser capaz de realizar esta optimización de manera segura si una función no es pura? (como en -faggressive-function-elimination)
  2. ¿Cómo puede un compilador determinar si una función es pura o no? (Como en la estrategia sugerida en el artículo de Wikipedia)

y finalmente:

  • Puede este tipo de optimización puede aplicar a cualquier lengua, o sólo cuando se cumplen ciertas condiciones?
  • ¿Es esta optimización una valiosa incluso para funciones extremadamente simples?
  • ¿Cuánta sobrecarga genera el almacenamiento y la recuperación de un valor de la pila?

Pido disculpas si estas son preguntas estúpidas o ilógicas. Son algunas de las cosas que me han llamado la atención últimamente. :)

+3

Esto se llama ** eliminación de subexpresiones comunes ** http://en.wikipedia.org/wiki/Common_subexpression_elimination – leppie

+0

Parece que falta la documentación para esa opción GNU Fortran, y sospecho que solo genera código incorrecto si la función tiene efectos secundarios no idempotentes. Una lectura rápida del manual de opciones de generación de código para Fortran de GNU me hace dudar seriamente de la calidad de su implementación, especialmente cosas como poner silenciosamente grandes variables locales en la memoria estática ... –

Respuesta

2

Descargo de responsabilidad: No soy compilador/optimizador, solo tengo una tendencia a echar un vistazo al código generado, y me gusta leer sobre eso, así que eso no es automático. Una búsqueda rápida no apareció mucho en la eliminación de función -faggressive, por lo que podría hacer algo de magia extra no explicada aquí.


Un optimizador puede

  • intento de inline la llamada de función (con generación de código de tiempo de enlace, esto funciona incluso a través de las unidades de compilación)
  • realizar la eliminación de subexpresiones comunes, y, posiblemente, efectos secundarios reordenando

La modificación de su ejemplo un poco, y lo hace en C++:

extern volatile int RW_A = 0; // see note below 

int foo(int a) { return a * a; } 
void bar(int x) { RW_A = x; } 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    bar(foo(2)); 
    bar(foo(2)); 
} 

Resuelve (pseudocódigo)

<register> = 4; 
RW_A = register; 
RW_A = register; 

(Nota: la lectura y la escritura en una variable volátil es una "efecto secundario observable", que el optimizador debe conservar en el mismo orden dado por el código.)


Modificar el ejemplo para foo a tiene un efecto secundario:

extern volatile int RW_A = 0; 
extern volatile int RW_B = 0; 
int accu = 1; 

int foo(int a) { accu *= 2; return a * a; } 
void bar(int x) { RW_A = x; } 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    bar(foo(2)); 
    bar(foo(2)); 

    RW_B = accu; 
    return 0; 
} 

genera el siguiente pseudocódigo:

registerA = accu; 
registerA += registerA; 
accu = registerA; 

registerA += registerA; 
registerC = 4; 
accu = registerA; 

RW_A = registerC; 
RW_A = registerC; 

RW_B = registerA; 

Observamos que la eliminación subexpresión común todavía se hace, y se separa de los efectos secundarios. Incrustar y reordenar permite separar los efectos secundarios de la parte "pura".

Tenga en cuenta que el compilador lee y vuelve a escribir con entusiasmo en accu, lo que no sería necesario. No estoy seguro del razonamiento aquí.


Para concluir:

Un compilador no es necesario para la prueba de pureza. Puede identificar los efectos secundarios que deben conservarse y luego transformar el resto a su gusto.

Estas optimizaciones son vale la pena, incluso para funciones triviales, ya que, entre otros,

  • recursos de la CPU y la memoria son compartidos, lo que se utiliza es posible quitar de otra persona
  • Duración de la batería
  • Las optimizaciones menores pueden ser puertas de acceso a las más grandes

La sobrecarga para un acceso a memoria de pila suele ser ~ 1 ciclo, ya que la parte superior de la pila suele estar en el nivel 1 ya.Tenga en cuenta que, por lo general, debe estar en negrita: puede ser "incluso mejor", ya que la lectura/escritura puede optimizarse, o puede ser peor ya que la mayor presión en la memoria caché L1 vacía otros datos importantes de vuelta a L2.

¿Dónde está el límite?

En teoría, el tiempo de compilación. En la práctica, la predictibilidad y la corrección del optimizador son compensaciones adicionales.


Todas las pruebas con VC2008, configuración de optimización predeterminada para la versión "Release".

+0

¡Respuesta impresionante! Ahora veo cómo un compilador no necesitaría probar la pureza. Con respecto a la sobrecarga del acceso a la memoria, esperaba que fuera bastante baja, muy buena. –

Cuestiones relacionadas