tipos programa procesos peephole para optimizar optimizacion niveles muerto ejemplos eficiente codigo c++ c performance optimization

c++ - procesos - Prácticas de codificación que permiten al compilador/optimizador hacer un programa más rápido



programa para optimizar codigo (30)

Hace muchos años, los compiladores de C no eran especialmente inteligentes. Como solución provisional, K & R inventó la palabra clave register , para insinuar al compilador, que tal vez sería una buena idea mantener esta variable en un registro interno. También hicieron que el operador terciario ayudara a generar un mejor código.

Con el paso del tiempo, los compiladores maduraron. Llegaron a ser muy inteligentes en cuanto a que su análisis de flujo les permitía tomar mejores decisiones sobre qué valores guardar en los registros de lo que posiblemente podrían hacer. La palabra clave de registro dejó de ser importante.

FORTRAN puede ser más rápido que C para algunos tipos de operaciones, debido a problemas de alias . En teoría, con una codificación cuidadosa, se puede evitar esta restricción para permitir que el optimizador genere código más rápido.

¿Qué prácticas de codificación están disponibles que pueden permitir que el compilador / optimizador genere código más rápido?

  • Identificando la plataforma y compilador que usa, sería apreciado.
  • ¿Por qué la técnica parece funcionar?
  • El código de muestra es recomendado.

Aquí hay una pregunta relacionada

[Editar] Esta pregunta no es sobre el proceso general para perfilar y optimizar. Supongamos que el programa se ha escrito correctamente, compilado con optimización completa, probado y puesto en producción. Puede haber construcciones en su código que impidan que el optimizador haga el mejor trabajo que pueda. ¿Qué puede hacer para refactorizar que eliminará estas prohibiciones y permitirá que el optimizador genere un código aún más rápido?

[Editar] Enlace relacionado al desplazamiento


Optimizaciones genéricas

Aquí como algunas de mis optimizaciones favoritas. De hecho, he aumentado los tiempos de ejecución y los tamaños de programa reducidos al usarlos.

Declarar funciones pequeñas como inline o macros

Cada llamada a una función (o método) incurre en gastos generales, como empujar variables a la pila. Algunas funciones pueden incurrir en una sobrecarga al regresar también. Una función o método ineficiente tiene menos declaraciones en su contenido que la sobrecarga combinada. Estos son buenos candidatos para inline, ya sea como #define macros o funciones en inline . (Sí, sé que en inline es solo una sugerencia, pero en este caso lo considero un recordatorio para el compilador).

Eliminar código muerto y redundante

Si el código no se utiliza o no contribuye al resultado del programa, deshágase de él.

Simplificar el diseño de algoritmos

Una vez eliminé un montón de código de ensamblado y tiempo de ejecución de un programa anotando la ecuación algebraica que estaba calculando y luego simplifiqué la expresión algebraica. La implementación de la expresión algebraica simplificada tomó menos espacio y tiempo que la función original.

Loop desenrollar

Cada bucle tiene una sobrecarga de incremento y verificación de terminación. Para obtener una estimación del factor de rendimiento, cuente el número de instrucciones en la tara (mínimo 3: incremento, verificación, goto inicio del bucle) y divida por el número de instrucciones dentro del bucle. Cuanto menor sea el número, mejor.

Editar: proporciona un ejemplo de desenrollado de bucle antes de:

unsigned int sum = 0; for (size_t i; i < BYTES_TO_CHECKSUM; ++i) { sum += *buffer++; }

Después de desenrollar:

unsigned int sum = 0; size_t i = 0; **const size_t STATEMENTS_PER_LOOP = 8;** for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**) { sum += *buffer++; // 1 sum += *buffer++; // 2 sum += *buffer++; // 3 sum += *buffer++; // 4 sum += *buffer++; // 5 sum += *buffer++; // 6 sum += *buffer++; // 7 sum += *buffer++; // 8 } // Handle the remainder: for (; i < BYTES_TO_CHECKSUM; ++i) { sum += *buffer++; }

En esta ventaja, se obtiene un beneficio secundario: se ejecutan más sentencias antes de que el procesador tenga que volver a cargar el caché de instrucciones.

He tenido resultados sorprendentes cuando desenrollé un loop a 32 declaraciones. Este fue uno de los cuellos de botella ya que el programa tuvo que calcular una suma de comprobación en un archivo de 2GB. Esta optimización combinada con la lectura de bloques mejoró el rendimiento de 1 hora a 5 minutos. El despliegue de bucles también proporcionó un excelente rendimiento en lenguaje ensamblador, mi memcpy fue mucho más rápido que el memcpy del compilador. - TM

Reducción de declaraciones if

Los procesadores odian las ramas o los saltos, ya que obligan al procesador a recargar su cola de instrucciones.

Aritmética Booleana ( Editado: formato de código aplicado al fragmento de código, ejemplo agregado)

Convierta las sentencias if en asignaciones booleanas. Algunos procesadores pueden ejecutar instrucciones condicionalmente sin ramificar:

bool status = true; status = status && /* first test */; status = status && /* second test */;

El cortocircuito del operador lógico AND ( && ) impide la ejecución de las pruebas si el status es false .

Ejemplo:

struct Reader_Interface { virtual bool write(unsigned int value) = 0; }; struct Rectangle { unsigned int origin_x; unsigned int origin_y; unsigned int height; unsigned int width; bool write(Reader_Interface * p_reader) { bool status = false; if (p_reader) { status = p_reader->write(origin_x); status = status && p_reader->write(origin_y); status = status && p_reader->write(height); status = status && p_reader->write(width); } return status; };

Asignación de factor de factor fuera de bucles

Si se crea una variable sobre la marcha dentro de un bucle, mueva la creación / asignación a antes del bucle. En la mayoría de los casos, la variable no necesita asignarse durante cada iteración.

Factor expresiones constantes fuera de bucles

Si un cálculo o valor de variable no depende del índice de bucle, muévalo fuera (antes) del bucle.

E / S en bloques

Leer y escribir datos en grandes bloques (bloques). Cuanto más grande, mejor. Por ejemplo, leer un octeto por vez es menos eficiente que leer 1024 octetos con una lectura.
Ejemplo:

static const char Menu_Text[] = "/n" "1) Print/n" "2) Insert new customer/n" "3) Destroy/n" "4) Launch Nasal Demons/n" "Enter selection: "; static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof(''/0''); //... std::cout.write(Menu_Text, Menu_Text_Length);

La eficacia de esta técnica se puede demostrar visualmente. :-)

No use la familia printf para datos constantes

Los datos constantes se pueden generar usando una escritura en bloque. La escritura con formato perderá tiempo escaneando el texto para formatear caracteres o procesar comandos de formateo. Vea el ejemplo de código anterior.

Formatee la memoria, luego escriba

Formatee una matriz char utilizando múltiples sprintf , luego use fwrite . Esto también permite que el diseño de datos se divida en "secciones constantes" y secciones variables. Piensa en combinar correspondencia .

Declarar texto constante (literales de cadena) como static const

Cuando las variables se declaran sin la static , algunos compiladores pueden asignar espacio en la pila y copiar los datos de la ROM. Estas son dos operaciones innecesarias. Esto se puede solucionar usando el prefijo static .

Por último, Código como el compilador

A veces, el compilador puede optimizar varias declaraciones pequeñas mejor que una versión complicada. Además, escribir código para ayudar al optimizador de compilación ayuda también. Si quiero que el compilador use instrucciones especiales de transferencia en bloque, escribiré un código que parece que debería usar las instrucciones especiales.


  1. Use the most local scope possible for all variable declarations.

  2. Use const whenever possible

  3. Dont use register unless you plan to profile both with and without it

The first 2 of these, especially #1 one help the optimizer analyze the code. It will especially help it to make good choices about what variables to keep in registers.

Blindly using the register keyword is as likely to help as hurt your optimization, It''s just too hard to know what will matter until you look at the assembly output or profile.

There are other things that matter to getting good performance out of code; designing your data structures to maximize cache coherency for instance. But the question was about the optimizer.


¡Escribe en variables locales y no en argumentos de salida! Esto puede ser una gran ayuda para evitar desaceleraciones de aliasing. Por ejemplo, si su código se ve como

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut) { for (int i=0; i<numFoo, i++) { barOut.munge(foo1, foo2[i]); } }

el compilador no sabe que foo1! = barOut, y por lo tanto tiene que volver a cargar foo1 cada vez a través del ciclo. Tampoco puede leer foo2 [i] hasta que finalice la escritura en barOut. Puede comenzar a jugar con punteros restringidos, pero es igual de efectivo (y mucho más claro) para hacer esto:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut) { Foo barTemp = barOut; for (int i=0; i<numFoo, i++) { barTemp.munge(foo1, foo2[i]); } barOut = barTemp; }

Suena tonto, pero el compilador puede ser mucho más inteligente al tratar con la variable local, ya que no puede superponerse en la memoria con ninguno de los argumentos. Esto puede ayudarlo a evitar la temida carga-hit-store (mencionada por Francis Boivin en este hilo).


Aquí hay una práctica de codificación para ayudar al compilador a crear código rápido: cualquier idioma, plataforma, compilador o cualquier problema:

No utilice ningún truco inteligente que fuerce, o incluso anime, al compilador a colocar variables en la memoria (incluidos el caché y los registros) como mejor le parezca. Primero escribe un programa que sea correcto y mantenible.

A continuación, perfila tu código.

Luego, y solo entonces, es posible que desee comenzar a investigar los efectos de decirle al compilador cómo usar la memoria. Haga 1 cambio a la vez y mida su impacto.

Espere estar decepcionado y tener que trabajar muy duro para pequeñas mejoras de rendimiento. Los compiladores modernos para lenguajes maduros como Fortran y C son muy, muy buenos. Si lees una cuenta de un ''truco'' para obtener un mejor rendimiento del código, ten en cuenta que los escritores del compilador también han leído sobre él y, si vale la pena hacerlo, probablemente lo haya implementado. Probablemente escribieron lo que leíste en primer lugar.


Dos técnicas de codificación que no vi en la lista anterior:

Bypass linker by writing code as an unique source

While separate compilation is really nice for compiling time, it is very bad when you speak of optimization. Basically the compiler can''t optimize beyond compilation unit, that is linker reserved domain.

But if you design well your program you can can also compile it through an unique common source. That is instead of compiling unit1.c and unit2.c then link both objects, compile all.c that merely #include unit1.c and unit2.c. Thus you will benefit from all the compiler optimizations.

It''s very like writing headers only programs in C++ (and even easier to do in C).

This technique is easy enough if you write your program to enable it from the beginning, but you must also be aware it change part of C semantic and you can meet some problems like static variables or macro collision. For most programs it''s easy enough to overcome the small problems that occurs. Also be aware that compiling as an unique source is way slower and may takes huge amount of memory (usually not a problem with modern systems).

Using this simple technique I happened to make some programs I wrote ten times faster!

Like the register keyword, this trick could also become obsolete soon. Optimizing through linker begin to be supported by compilers gcc: Link time optimization .

Separate atomic tasks in loops

This one is more tricky. It''s about interaction between algorithm design and the way optimizer manage cache and register allocation. Quite often programs have to loop over some data structure and for each item perform some actions. Quite often the actions performed can be splitted between two logically independent tasks. If that is the case you can write exactly the same program with two loops on the same boundary performing exactly one task. In some case writing it this way can be faster than the unique loop (details are more complex, but an explanation can be that with the simple task case all variables can be kept in processor registers and with the more complex one it''s not possible and some registers must be written to memory and read back later and the cost is higher than additional flow control).

Be careful with this one (profile performances using this trick or not) as like using register it may as well give lesser performances than improved ones.


El optimizador no controla realmente el rendimiento de su programa, usted sí. Use algoritmos y estructuras apropiados y perfil, perfil, perfil.

Dicho esto, no debe realizar un bucle interno en una función pequeña desde un archivo en otro archivo, ya que eso impide que esté en línea.

Evite tomar la dirección de una variable si es posible. Pedir un puntero no es "gratuito", ya que significa que la variable debe mantenerse en la memoria. Incluso una matriz puede mantenerse en registros si se evitan los punteros, esto es esencial para vectorizar.

Lo que lleva al siguiente punto, lea el manual ^ # $ @ ! GCC puede vectorizar código C simple si espolvorea __restrict__ aquí y __attribute__( __aligned__ ) allí. Si desea algo muy específico del optimizador, es posible que tenga que ser específico.


El orden en que recorre la memoria puede tener un profundo impacto en el rendimiento y los compiladores no son realmente buenos para resolverlo y solucionarlo. Debe tener conciencia de las preocupaciones de la localidad de caché cuando escribe código si le importa el rendimiento. Por ejemplo, las matrices bidimensionales en C se asignan en formato de fila mayor. El desplazamiento de matrices en formato de columna principal tenderá a hacer que tenga más errores de caché y que su programa tenga más memoria vinculada que el procesador encuadernado:

#define N 1000000; int matrix[N][N] = { ... }; //awesomely fast long sum = 0; for(int i = 0; i < N; i++){ for(int j = 0; j < N; j++){ sum += matrix[i][j]; } } //painfully slow long sum = 0; for(int i = 0; i < N; i++){ for(int j = 0; j < N; j++){ sum += matrix[j][i]; } }


En el caso de los sistemas integrados y el código escrito en C / C ++, intento evitar la asignación de memoria dinámica tanto como sea posible. La razón principal por la que hago esto no es necesariamente el rendimiento, pero esta regla general tiene implicaciones de rendimiento.

Los algoritmos utilizados para administrar el montón son notoriamente lentos en algunas plataformas (p. Ej., Vxworks). Peor aún, el tiempo que demora en regresar de una llamada a malloc depende en gran medida del estado actual del montón. Por lo tanto, cualquier función que llame a malloc va a tener un impacto en el rendimiento que no se puede explicar fácilmente. Ese golpe de rendimiento puede ser mínimo si el montón todavía está limpio, pero después de que el dispositivo funcione por un tiempo, el montón puede fragmentarse. Las llamadas van a llevar más tiempo y no se puede calcular fácilmente cómo el rendimiento se degradará con el tiempo. Realmente no se puede producir una estimación de caso peor. El optimizador tampoco puede brindarle ayuda en este caso. Para empeorar las cosas, si el montón se fragmenta demasiado, las llamadas comenzarán a fallar por completo. La solución es usar pools de memoria (por ejemplo, rebanadas glib ) en lugar del montón. Las llamadas de asignación van a ser mucho más rápidas y deterministas si lo haces bien.


En la mayoría de los procesadores modernos, el cuello de botella más grande es la memoria.

Aliasing: Load-Hit-Store puede ser devastador en un ciclo cerrado. Si está leyendo una ubicación de la memoria y escribiendo en otra y sabe que son disjuntos, colocar cuidadosamente una palabra clave alias en los parámetros de la función realmente puede ayudar al compilador a generar código más rápido. Sin embargo, si las regiones de memoria se superponen y usted usó ''alias'', ¡tendrá una buena sesión de depuración de comportamientos indefinidos!

Cache-miss: No estoy seguro de cómo puede ayudar al compilador, ya que es principalmente algorítmico, pero hay aspectos intrínsecos a la memoria de captación previa.

Tampoco trate de convertir los valores de punto flotante a int y viceversa, ya que utilizan diferentes registros y la conversión de un tipo a otro significa llamar a la instrucción de conversión real, escribir el valor en la memoria y leerlo en el conjunto de registros adecuado .


Escribí un compilador de optimización de C y aquí hay algunas cosas muy útiles a tener en cuenta:

  1. Haz la mayoría de las funciones estáticas. Esto permite que la propagación constante interprocedural y el análisis de alias hagan su trabajo; de lo contrario, el compilador debe suponer que la función puede invocarse desde fuera de la unidad de traducción con valores completamente desconocidos para los parámetros. Si observa las conocidas bibliotecas de código abierto, todas ellas marcan funciones estáticas, excepto las que realmente necesitan ser externas.

  2. Si se usan variables globales, márquelas estáticas y constantes si es posible. Si se inicializan una vez (solo lectura), es mejor usar una lista de inicializadores como static const int VAL [] = {1,2,3,4}, de lo contrario, el compilador podría no descubrir que las variables son en realidad constantes inicializadas y no podrá reemplazar cargas de la variable con las constantes.

  3. NUNCA use un goto en el interior de un bucle, el bucle ya no será reconocido por la mayoría de los compiladores y no se aplicará ninguna de las optimizaciones más importantes.

  4. Utilice los parámetros del puntero solo si es necesario, y márquelos de ser posible. Esto ayuda mucho al análisis de alias porque el programador garantiza que no hay alias (el análisis de alias interprocedimiento suele ser muy primitivo). Los objetos struct muy pequeños deben pasarse por valor, no por referencia.

  5. Use matrices en lugar de punteros siempre que sea posible, especialmente dentro de los bucles (a [i]). Normalmente, una matriz ofrece más información para el análisis de alias y, después de algunas optimizaciones, se generará el mismo código de todos modos (si es curioso, busque la reducción de la intensidad del bucle). Esto también aumenta la posibilidad de que se aplique el movimiento de código invariante en bucle.

  6. Intente elevar fuera de las llamadas de bucle a funciones grandes o funciones externas que no tengan efectos secundarios (no dependa de la iteración actual del bucle). Las funciones pequeñas en muchos casos están en línea o convertidas en intrínsecas que son fáciles de elevar, pero las funciones grandes pueden parecer para el compilador tener efectos secundarios cuando en realidad no lo hacen. Los efectos secundarios para las funciones externas son completamente desconocidos, con la excepción de algunas funciones de la biblioteca estándar que algunas veces son modeladas por algunos compiladores, lo que hace posible el movimiento de códigos invariantes por bucle.

  7. Al escribir pruebas con múltiples condiciones, coloque primero la más probable. if (a || b || c) debería ser if (b || a || c) si es más probable que b sea ​​cierto que los demás. Por lo general, los compiladores no saben nada sobre los posibles valores de las condiciones y qué ramas se toman más (podrían conocerse mediante el uso de información de perfil, pero pocos programadores la usan).

  8. Usar un interruptor es más rápido que hacer una prueba como si (a || b || ... || z). Verifique primero si su compilador hace esto automáticamente, algunos lo hacen y es más legible para tener el if .


Intente programar utilizando la única asignación estática tanto como sea posible. SSA es exactamente igual a lo que terminas en la mayoría de los lenguajes de programación funcionales, y eso es a lo que la mayoría de los compiladores convierten tu código para hacer sus optimizaciones porque es más fácil trabajar con él. Al hacer esto, salen a la luz lugares donde el compilador puede confundirse. También hace que todos, excepto los peores asignadores de registros, funcionen tan bien como los mejores asignadores de registros, y te permite depurar más fácilmente porque casi nunca tienes que preguntarte de dónde proviene la variable, ya que solo se asignó un lugar.
Evita las variables globales.

Cuando trabaje con datos por referencia o puntero, extraiga eso en variables locales, haga su trabajo y luego cópielo nuevamente. (a menos que tenga una buena razón para no hacerlo)

Haga uso de la comparación casi gratuita contra 0 que la mayoría de los procesadores le dan al hacer operaciones matemáticas o lógicas. Casi siempre obtienes una bandera para == 0 y <0, de la cual puedes obtener fácilmente 3 condiciones:

x= f(); if(!x){ a(); } else if (x<0){ b(); } else { c(); }

casi siempre es más barato que probar otras constantes.

Otro truco es usar la resta para eliminar una comparación en la prueba de rango.

#define FOO_MIN 8 #define FOO_MAX 199 int good_foo(int foo) { unsigned int bar = foo-FOO_MIN; int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0; return rc; }

Esto a menudo puede evitar un salto en los lenguajes que provocan un cortocircuito en las expresiones booleanas y evita que el compilador tenga que tratar de descubrir cómo manejar el resultado de la primera comparación mientras hace el segundo y luego combinarlos. Esto puede parecer que tiene el potencial de agotar un registro adicional, pero casi nunca lo hace. A menudo ya no necesitas foo nunca más, y si lo haces no se usa todavía, así que puede ir allí.

Cuando use las funciones de cadena en c (strcpy, memcpy, ...) recuerde lo que devuelve: ¡el destino! A menudo puede obtener un mejor código ''olvidando'' su copia del puntero a su destino y simplemente recuperarlo del retorno de estas funciones.

Nunca pase por alto la oportunidad de devolver exactamente lo mismo que devolvió la última función que llamó. Los compiladores no son tan buenos para recoger eso:

foo_t * make_foo(int a, int b, int c) { foo_t * x = malloc(sizeof(foo)); if (!x) { // return NULL; return x; // x is NULL, already in the register used for returns, so duh } x->a= a; x->b = b; x->c = c; return x; }

Por supuesto, podría revertir la lógica sobre eso si solo tiene un punto de retorno.

(trucos que recordé más tarde)

Declarar funciones como estáticas siempre que pueda siempre es una buena idea. Si el compilador puede comprobarse a sí mismo que ha contabilizado a cada llamante de una función en particular, entonces puede romper las convenciones de llamada para esa función en nombre de la optimización. Los compiladores a menudo pueden evitar mover los parámetros a registros o posiciones de pila que las funciones llamadas normalmente esperan que tengan sus parámetros (debe desviarse tanto en la función llamada como en la ubicación de todos los llamantes para hacer esto). El compilador a menudo también puede aprovechar el conocimiento de qué memoria y registros necesitará la función llamada y evitar generar código para preservar los valores de las variables que se encuentran en registros o ubicaciones de memoria que la función llamada no molesta. Esto funciona particularmente bien cuando hay pocas llamadas a una función. Esto obtiene gran parte de la ventaja del código de enlining, pero sin incluir en línea.


La gran mayoría del código que las personas escriben estará vinculado a E / S (creo que todo el código que he escrito por dinero en los últimos 30 años ha estado tan limitado), por lo que las actividades del optimizador para la mayoría de las personas serán académicas.

Sin embargo, le recordaría a la gente que para que el código sea optimizado, debe decirle al compilador que lo optimice; mucha gente (incluyéndome cuando lo olvide) publica puntos de referencia de C ++ que no tienen sentido sin que se habilite el optimizador.


Una pequeña propina tonta, pero que te ahorrará algunas cantidades microscópicas de velocidad y código.

Siempre pase los argumentos de la función en el mismo orden.

Si tiene f_1 (x, y, z) que llama a f_2, declare f_2 como f_2 (x, y, z). No lo declare como f_2 (x, z, y).

La razón de esto es que la plataforma C / C ++ ABI (Convención de llamadas AKA) promete pasar argumentos en registros particulares y ubicaciones de pila. Cuando los argumentos ya están en los registros correctos, entonces no tiene que moverlos.

Mientras leía el código desensamblado, he visto un registro ridículo porque la gente no siguió esta regla.


use const correctness tanto como sea posible en su código. Permite al compilador optimizar mucho mejor.

En este documento hay muchos otros consejos de optimización: optimizaciones de CPP (aunque un documento un poco viejo)

reflejos:

  • usar listas de inicialización de constructor
  • usar operadores de prefijos
  • usar constructores explícitos
  • funciones en línea
  • evitar objetos temporales
  • tenga en cuenta el costo de las funciones virtuales
  • devolver objetos a través de parámetros de referencia
  • considerar por asignación de clase
  • considerar los asignadores de contenedor STL
  • la optimización de ''miembro vacío''
  • etc


A neat technique I learned from @MSalters comment on this answer allows compilers to do copy elision even when returning different objects according to some condition:

// before BigObject a, b; if(condition) return a; else return b; // after BigObject a, b; if(condition) swap(a,b); return a;


Don''t do the same work over and over again!

A common antipattern that I see goes along these lines:

void Function() { MySingleton::GetInstance()->GetAggregatedObject()->DoSomething(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat(); MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain(); }

The compiler actually has to call all of those functions all of the time. Assuming you, the programmer, knows that the aggregated object isn''t changing over the course of these calls, for the love of all that is holy...

void Function() { MySingleton* s = MySingleton::GetInstance(); AggregatedObject* ao = s->GetAggregatedObject(); ao->DoSomething(); ao->DoSomethingElse(); ao->DoSomethingCool(); ao->DoSomethingReallyNeat(); ao->DoSomethingYetAgain(); }

In the case of the singleton getter the calls may not be too costly, but it is certainly a cost (typically, "check to see if the object has been created, if it hasn''t, create it, then return it). The more complicated this chain of getters becomes, the more wasted time we''ll have.


For performance, focus first on writing maintenable code - componentized, loosely coupled, etc, so when you have to isolate a part either to rewrite, optimize or simply profile, you can do it without much effort.

Optimizer will help your program''s performance marginally.


Here''s my second piece of optimisation advice. As with my first piece of advice this is general purpose, not language or processor specific.

Read the compiler manual thoroughly and understand what it is telling you. Use the compiler to its utmost.

I agree with one or two of the other respondents who have identified selecting the right algorithm as critical to squeezing performance out of a program. Beyond that the rate of return (measured in code execution improvement) on the time you invest in using the compiler is far higher than the rate of return in tweaking the code.

Yes, compiler writers are not from a race of coding giants and compilers contain mistakes and what should, according to the manual and according to compiler theory, make things faster sometimes makes things slower. That''s why you have to take one step at a time and measure before- and after-tweak performance.

And yes, ultimately, you might be faced with a combinatorial explosion of compiler flags so you need to have a script or two to run make with various compiler flags, queue the jobs on the large cluster and gather the run time statistics. If it''s just you and Visual Studio on a PC you will run out of interest long before you have tried enough combinations of enough compiler flags.

Saludos

marca

When I first pick up a piece of code I can usually get a factor of 1.4 -- 2.0 times more performance (ie the new version of the code runs in 1/1.4 or 1/2 of the time of the old version) within a day or two by fiddling with compiler flags. Granted, that may be a comment on the lack of compiler savvy among the scientists who originate much of the code I work on, rather than a symptom of my excellence. Having set the compiler flags to max (and it''s rarely just -O3) it can take months of hard work to get another factor of 1.05 or 1.1


I have long suspected, but never proved that declaring arrays so that they hold a power of 2, as the number of elements, enables the optimizer to do a strength reduction by replacing a multiply by a shift by a number of bits, when looking up individual elements.


I was reminded of something that I encountered once, where the symptom was simply that we were running out of memory, but the result was substantially increased performance (as well as huge reductions in memory footprint).

The problem in this case was that the software we were using made tons of little allocations. Like, allocating four bytes here, six bytes there, etc. A lot of little objects, too, running in the 8-12 byte range. The problem wasn''t so much that the program needed lots of little things, it''s that it allocated lots of little things individually, which bloated each allocation out to (on this particular platform) 32 bytes.

Part of the solution was to put together an Alexandrescu-style small object pool, but extend it so I could allocate arrays of small objects as well as individual items. This helped immensely in performance as well since more items fit in the cache at any one time.

The other part of the solution was to replace the rampant use of manually-managed char* members with an SSO (small-string optimization) string. The minimum allocation being 32 bytes, I built a string class that had an embedded 28-character buffer behind a char*, so 95% of our strings didn''t need to do an additional allocation (and then I manually replaced almost every appearance of char* in this library with this new class, that was fun or not). This helped a ton with memory fragmentation as well, which then increased the locality of reference for other pointed-to objects, and similarly there were performance gains.


I''ve actually seen this done in SQLite and they claim it results in performance boosts ~5%: Put all your code in one file or use the preprocessor to do the equivalent to this. This way the optimizer will have access to the entire program and can do more interprocedural optimizations.


If you''ve got small functions you call repeatedly, i have in the past got large gains by putting them in headers as "static inline". Function calls on the ix86 are surprisingly expensive.

Reimplementing recursive functions in a non-recursive way using an explicit stack can also gain a lot, but then you really are in the realm of development time vs gain.


Most modern compilers should do a good job speeding up tail recursion , because the function calls can be optimized out.

Ejemplo:

int fac2(int x, int cur) { if (x == 1) return cur; return fac2(x - 1, cur * x); } int fac(int x) { return fac2(x, 1); }

Of course this example doesn''t have any bounds checking.

Late Edit

While I have no direct knowledge of the code; it seems clear that the requirements of using CTEs on SQL Server were specifically designed so that it can optimize via tail-end recursion.


One optimization i have used in C++ is creating a constructor that does nothing. One must manually call an init() in order to put the object into a working state.

This has benefit in the case where I need a large vector of these classes.

I call reserve() to allocate the space for the vector, but the constructor does not actually touch the page of memory the object is on. So I have spent some address space, but not actually consumed a lot of physical memory. I avoid the page faults associated the associated construction costs.

As i generate objects to fill the vector, I set them using init(). This limits my total page faults, and avoids the need to resize() the vector while filling it.


One thing I''ve done is try to keep expensive actions to places where the user might expect the program to delay a bit. Overall performance is related to responsiveness, but isn''t quite the same, and for many things responsiveness is the more important part of performance.

The last time I really had to do improvements in overall performance, I kept an eye out for suboptimal algorithms, and looked for places that were likely to have cache problems. I profiled and measured performance first, and again after each change. Then the company collapsed, but it was interesting and instructive work anyway.


Put small and/or frequently called functions at the top of the source file. That makes it easier for the compiler to find opportunities for inlining.


When DEC came out with its alpha processors, there was a recommendation to keep the number of arguments to a function under 7, as the compiler would always try to put up to 6 arguments in registers automatically.


You''re getting good answers here, but they assume your program is pretty close to optimal to begin with, and you say

Assume that the program has been written correctly, compiled with full optimization, tested and put into production.

In my experience, a program may be written correctly, but that does not mean it is near optimal. It takes extra work to get to that point.

If I can give an example, this answer shows how a perfectly reasonable-looking program was made over 40 times faster by macro-optimization . Big speedups can''t be done in every program as first written, but in many (except for very small programs), it can, in my experience.

After that is done, micro-optimization (of the hot-spots) can give you a good payoff.


i use intel compiler. on both Windows and Linux.

when more or less done i profile the code. then hang on the hotspots and trying to change the code to allow compiler make a better job.

if a code is a computational one and contain a lot of loops - vectorization report in intel compiler is very helpful - look for ''vec-report'' in help.

so the main idea - polish the performance critical code. as for the rest - priority to be correct and maintainable - short functions, clear code that could be understood 1 year later.