Hace muchos años, los compiladores de C no eran particularmente inteligentes. Como solución alternativa, K&R inventó la palabra clave register , para sugerirle al compilador que quizás sería una buena idea mantener esta variable en un registro interno. También crearon el operador terciario para ayudar a generar un mejor código.
Con el paso del tiempo, los compiladores maduraron. Se volvieron muy inteligentes en el sentido de que su análisis de flujo les permitía tomar mejores decisiones sobre qué valores mantener en los registros de lo que podría hacer usted. 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 al compilador / optimizador generar código más rápido?
- Se agradecería identificar la plataforma y el compilador que utiliza.
- ¿Por qué la técnica parece funcionar?
- Se recomienda un código de muestra.
Aquí hay una pregunta relacionada
[Editar] Esta pregunta no trata sobre el proceso general para perfilar y optimizar. Suponga 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 prohíben al optimizador hacer el mejor trabajo posible. ¿Qué puede hacer para refactorizar que elimine estas prohibiciones y permita que el optimizador genere un código aún más rápido?
fuente
register
de hecho, hizo que el código sensible al rendimiento fuera más portátil al combatir compiladores deficientes.Respuestas:
Escriba en variables locales y no en argumentos de salida. Esto puede ser de gran ayuda para evitar las ralentizaciones de alias. Por ejemplo, si su código se parece a
el compilador no sabe que foo1! = barOut, y por lo tanto tiene que recargar foo1 cada vez que pasa por el ciclo. Tampoco puede leer foo2 [i] hasta que finalice la escritura en barOut. Podría comenzar a jugar con punteros restringidos, pero es igual de efectivo (y mucho más claro) hacer esto:
Suena tonto, pero el compilador puede ser mucho más inteligente al manejar la variable local, ya que no es posible que se superponga en la memoria con ninguno de los argumentos. Esto puede ayudarlo a evitar el temido load-hit-store (mencionado por Francis Boivin en este hilo).
fuente
Aquí hay una práctica de codificación para ayudar al compilador a crear código rápido: cualquier lenguaje, plataforma, compilador, problema:
hacer no utilizar ningún tipo de trucos ingeniosos que la fuerza, o incluso alentar, el compilador para sentar las variables en la memoria (incluyendo la caché y los registros) como mejor le parezca. Primero escriba un programa que sea correcto y que se pueda mantener.
A continuación, perfile tu código.
Entonces, y solo entonces, es posible que desee comenzar a investigar los efectos de decirle al compilador cómo usar la memoria. Realice 1 cambio a la vez y mida su impacto.
Espere sentirse decepcionado y tener que trabajar muy duro para obtener pequeñas mejoras de rendimiento. Los compiladores modernos para lenguajes maduros como Fortran y C son muy, muy buenos. Si lee el relato de un 'truco' para obtener un mejor rendimiento del código, tenga en cuenta que los escritores del compilador también lo han leído y, si vale la pena hacerlo, probablemente lo hayan implementado. Probablemente escribieron lo que leíste en primer lugar.
fuente
&
frente%
a potencias de dos (rara vez, o nunca, optimizado, pero puede tener un impacto significativo en el rendimiento). Si lee un truco para el desempeño, la única forma de saber si funciona es hacer el cambio y medir el impacto. Nunca asuma que el compilador optimizará algo para usted.n
, reemplaza gcc% n
con& (n-1)
incluso cuando se desactiva la optimización . Eso no es exactamente "rara vez, si es que alguna vez" ...El orden en que recorre la memoria puede tener un impacto profundo en el rendimiento y los compiladores no son realmente buenos para averiguarlo y solucionarlo. Debe ser consciente de las preocupaciones sobre la ubicación de la caché cuando escribe código si le preocupa el rendimiento. Por ejemplo, las matrices bidimensionales en C se asignan en formato de fila principal. Atravesar matrices en formato de columna principal tenderá a hacer que tenga más fallas de caché y hará que su programa esté más limitado a la memoria que al procesador:
fuente
-floop-interchange
que cambiará un bucle interno y externo si el optimizador lo considera rentable.Optimizaciones genéricas
Aquí algunas de mis optimizaciones favoritas. De hecho, he aumentado los tiempos de ejecución y reducido el tamaño de los programas al usarlos.
Declarar pequeñas funciones como
inline
macrosCada llamada a una función (o método) genera una sobrecarga, como insertar variables en la pila. Algunas funciones también pueden incurrir en gastos generales a la devolución. Una función o método ineficaz tiene menos declaraciones en su contenido que la sobrecarga combinada. Estos son buenos candidatos para la inserción, ya sea como
#define
macros oinline
funciones. (Sí, sé queinline
es solo una sugerencia, pero en este caso lo considero como un recordatorio para el compilador).Elimina el código muerto y redundante
Si el código no se utiliza o no contribuye al resultado del programa, elimínelo.
Simplifique el diseño de algoritmos
Una vez eliminé mucho código ensamblador y tiempo de ejecución de un programa escribiendo la ecuación algebraica que estaba calculando y luego simplifiqué la expresión algebraica. La implementación de la expresión algebraica simplificada ocupó menos espacio y tiempo que la función original.
Desenrollado de bucle
Cada bucle tiene una sobrecarga de comprobación de incrementos y terminaciones. Para obtener una estimación del factor de rendimiento, cuente el número de instrucciones en la sobrecarga (mínimo 3: incremento, verificación, ir al inicio del ciclo) y divida por el número de declaraciones dentro del ciclo. Cuanto menor sea el número, mejor.
Editar: proporcione un ejemplo de desenrollado de bucle Antes:
Después de desenrollar:
En esta ventaja, se obtiene un beneficio secundario: se ejecutan más sentencias antes de que el procesador tenga que recargar la caché de instrucciones.
Tuve resultados asombrosos cuando desenrollé un ciclo a 32 declaraciones. Este fue uno de los cuellos de botella ya que el programa tuvo que calcular una suma de verificació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 desenrollado de bucles también proporcionó un rendimiento excelente en lenguaje ensamblador, mi
memcpy
fue mucho más rápido que el del compiladormemcpy
. - TMReducción de
if
declaracionesLos procesadores odian las ramificaciones o saltos, ya que obliga al procesador a recargar su cola de instrucciones.
Aritmética booleana ( Editado: formato de código aplicado al fragmento de código, ejemplo agregado)
Convertir
if
declaraciones en asignaciones booleanas. Algunos procesadores pueden ejecutar instrucciones de forma condicional sin ramificar:El cortocircuito del operador lógico AND (
&&
) impide la ejecución de las pruebas sistatus
esfalse
.Ejemplo:
Factorizar la asignación de variables fuera de los bucles
Si una variable se crea sobre la marcha dentro de un ciclo, mueva la creación / asignación antes del ciclo. En la mayoría de los casos, no es necesario asignar la variable durante cada iteración.
Factorizar expresiones constantes fuera de los bucles
Si un cálculo o valor de variable no depende del índice del ciclo, muévalo fuera (antes) del ciclo.
E / S en bloques
Leer y escribir datos en grandes porciones (bloques). Cuanto más grande, mejor. Por ejemplo, leer un octecto a la vez es menos eficiente que leer 1024 octetos con una lectura.
Ejemplo:
La eficacia de esta técnica se puede demostrar visualmente. :-)
No use la
printf
familia para datos constantesLos datos constantes se pueden generar mediante una escritura en bloque. La escritura formateada perderá tiempo escaneando el texto en busca de caracteres de formato o comandos de formato de procesamiento. Vea el ejemplo de código anterior.
Formatee en la memoria, luego escriba
Formatee en una
char
matriz usando multiplesprintf
, luego usefwrite
. Esto también permite dividir el diseño de datos en "secciones constantes" y secciones variables. Piense en la combinación de correspondencia .Declare texto constante (cadenas literales) como
static const
Cuando las variables se declaran sin el
static
, algunos compiladores pueden asignar espacio en la pila y copiar los datos de la ROM. Estas son dos operaciones innecesarias. Esto se puede arreglar usando elstatic
prefijo.Por último, código como lo haría 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 compilador a optimizar también ayuda. 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.
fuente
fprintf
formatos en un búfer separado luego generen el búfer. Unfprintf
formato simplificado (para uso de memoria) generaría todo el texto sin formato, luego formatearía y generaría, y se repetiría hasta que se procese toda la cadena de formato, haciendo así 1 llamada de salida para cada tipo de salida (formateado vs. sin formato). Otras implementaciones necesitarían asignar memoria dinámicamente para cada llamada para contener la nueva cadena completa (lo cual es malo en el entorno de sistemas integrados). Mi sugerencia reduce el número de salidas.El optimizador no tiene realmente el control del rendimiento de su programa, sino usted. Utilice algoritmos y estructuras adecuados y perfil, perfil, perfil.
Dicho esto, no debe hacer un bucle interno en una función pequeña de un archivo en otro archivo, ya que eso evita que esté en línea.
Evite tomar la dirección de una variable si es posible. Solicitar un puntero no es "gratuito", ya que significa que la variable debe mantenerse en la memoria. Incluso una matriz se puede mantener en registros si evita los punteros; esto es esencial para la vectorización.
Lo que lleva al siguiente punto, lea el manual ^ # $ @ . GCC puede vectorizar código C simple si rocía
__restrict__
aquí y__attribute__( __aligned__ )
allá. Si desea algo muy específico del optimizador, es posible que deba ser específico.fuente
A.c
insertarse enB.c
.En la mayoría de los procesadores modernos, el mayor cuello de botella es la memoria.
Aliasing: Load-Hit-Store puede ser devastador en un circuito cerrado. Si está leyendo una ubicación de memoria y escribiendo en otra y sabe que no están unidas, poner con cuidado una palabra clave de 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 usó 'alias', ¡tendrá una buena sesión de depuración de comportamientos indefinidos!
Cache-miss: No estoy realmente seguro de cómo puede ayudar al compilador, ya que es principalmente algorítmico, pero hay elementos intrínsecos para obtener memoria previa.
Además, no intente convertir valores de punto flotante a int y viceversa, ya que usan registros diferentes y convertir 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. .
fuente
La gran mayoría del código que la gente escribe estará ligado a E / S (creo que todo el código que he escrito por dinero en los últimos 30 años lo ha sido), por lo que las actividades del optimizador para la mayoría de la gente serán académicas.
Sin embargo, quisiera recordarle a la gente que para que el código se optimice, debe decirle al compilador que lo optimice; mucha gente (incluyéndome a mí cuando lo olvido) publica aquí los puntos de referencia de C ++ que no tienen sentido sin que el optimizador esté habilitado.
fuente
use la corrección constante 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 antiguo)
Destacar:
fuente
const
yrestrict
no está definido. Entonces, un compilador podría optimizar de manera diferente en tal caso.const
unaconst
referencia o unconst
puntero a un noconst
objeto está bien definido. modificar unconst
objeto real (es decir, uno declarado comoconst
originalmente) no lo es.Intente programar utilizando asignación única estática tanto como sea posible. SSA es exactamente igual a lo que se obtiene en la mayoría de los lenguajes de programación funcionales, y eso es a lo que la mayoría de los compiladores convierten su código para realizar sus optimizaciones porque es más fácil trabajar con él. Al hacer esto, los lugares donde el compilador podría confundirse se revelan. También hace que todos los asignadores de registros, excepto los peores, funcionen tan bien como los mejores asignadores de registros, y le permite depurar más fácilmente porque casi nunca tiene que preguntarse de dónde obtuvo una variable su valor, ya que solo había un lugar donde se asignó.
Evite las variables globales.
Cuando trabaje con datos por referencia o puntero, introdúzcalos en variables locales, haga su trabajo y luego cópielos. (a menos que tenga una buena razón para no hacerlo)
Utilice la comparación casi gratuita con 0 que la mayoría de los procesadores le ofrecen al realizar operaciones matemáticas o lógicas. Casi siempre obtiene una bandera para == 0 y <0, de la cual puede obtener fácilmente 3 condiciones:
es casi siempre más económico que probar otras constantes.
Otro truco consiste en utilizar la resta para eliminar una comparación en la prueba de rango.
Esto a menudo puede evitar un salto en los lenguajes que hacen cortocircuitos en las expresiones booleanas y evita que el compilador tenga que tratar de averiguar cómo manejar mantenerse al día con el resultado de la primera comparación mientras hace la segunda y luego los combina. Esto puede parecer que tiene el potencial de utilizar un registro adicional, pero casi nunca lo hace. De todos modos, a menudo ya no necesita foo, y si lo hace, rc aún no se usa, por lo que puede ir allí.
Cuando use las funciones de cadena en c (strcpy, memcpy, ...) recuerde lo que devuelven: ¡el destino! A menudo, puede obtener un mejor código "olvidando" su copia del puntero al destino y simplemente recupéralo cuando regresen 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:
Por supuesto, podría revertir la lógica en eso si y solo tuviera un punto de retorno.
(trucos que recordé más tarde)
Siempre es una buena idea declarar las funciones como estáticas cuando sea posible. Si el compilador puede probarse a sí mismo que ha contabilizado a cada llamador 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 parámetros a registros o posiciones de pila en las que las funciones llamadas normalmente esperan que estén sus parámetros (tiene que desviarse tanto en la función llamada como en la ubicación de todos los llamadores para hacer esto). El compilador a menudo también puede aprovechar el saber qué memoria y registros necesitará la función llamada y evitar generar código para preservar los valores de las variables que están en registros o ubicaciones de memoria que la función llamada no perturba. Esto funciona particularmente bien cuando hay pocas llamadas a una función.
fuente
Escribí un compilador C optimizado y aquí hay algunas cosas muy útiles a considerar:
Haga que la mayoría de las funciones sean estáticas. Esto permite que la propagación constante entre procedimientos y el análisis de alias hagan su trabajo; de lo contrario, el compilador debe suponer que la función se puede llamar 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 marcan las funciones como estáticas, excepto las que realmente necesitan ser externas.
Si se utilizan variables globales, márquelas estáticas y constantes si es posible. Si se inicializan una vez (solo lectura), es mejor usar una lista de inicializador 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 las cargas de la variable con las constantes.
NUNCA use un goto al 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.
Utilice los parámetros de puntero solo si es necesario y márquelos como restringidos si es posible. Esto ayuda mucho al análisis de alias porque el programador garantiza que no hay alias (el análisis de alias entre procedimientos suele ser muy primitivo). Los objetos de estructura muy pequeños deben pasarse por valor, no por referencia.
Utilice matrices en lugar de punteros siempre que sea posible, especialmente dentro de bucles (a [i]). Una matriz generalmente 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 (busque la reducción de la fuerza del bucle si tiene curiosidad). Esto también aumenta la posibilidad de que se aplique un movimiento de código invariante en bucle.
Intente realizar llamadas fuera del bucle a funciones grandes o funciones externas que no tengan efectos secundarios (no dependa de la iteración del bucle actual). En muchos casos, las funciones pequeñas están integradas 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 los tienen. Los efectos secundarios de las funciones externas son completamente desconocidos, con la excepción de algunas funciones de la biblioteca estándar que a veces son modeladas por algunos compiladores, lo que hace posible el movimiento de código invariante en bucle.
Al escribir pruebas con múltiples condiciones, coloque la más probable en primer lugar. si (a || b || c) debería ser si (b || a || c) si b es más probable que sea cierto que los demás. Los compiladores generalmente no saben nada sobre los posibles valores de las condiciones y qué ramas se toman más (podrían conocerse usando información de perfil, pero pocos programadores la usan).
Usar un interruptor es más rápido que hacer una prueba como si (a || b || ... || z). Primero verifique si su compilador hace esto automáticamente, algunos lo hacen y es más legible tener el if .
fuente
En el caso de sistemas integrados y código escrito en C / C ++, trato de 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 en el rendimiento.
Los algoritmos utilizados para administrar el montón son notoriamente lentos en algunas plataformas (por ejemplo, vxworks). Peor aún, el tiempo que se tarda en volver 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 sufrirá un impacto en el rendimiento que no se puede explicar fácilmente. Ese impacto en el rendimiento puede ser mínimo si el montón todavía está limpio, pero después de que el dispositivo se ejecuta durante un tiempo, el montón puede fragmentarse. Las llamadas tardarán más y no se puede calcular fácilmente cómo se degradará el rendimiento con el tiempo. Realmente no se puede producir una estimación del peor caso. El optimizador tampoco puede ofrecerle 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 grupos de memoria (por ejemplo,cortes simplistas ) en lugar del montón. Las llamadas de asignación serán mucho más rápidas y deterministas si lo haces bien.
fuente
Un pequeño consejo tonto, pero que le ahorrará algunas cantidades microscópicas de velocidad y código.
Pase siempre 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 (también conocida como convención de llamadas) promete pasar argumentos en registros particulares y ubicaciones de pila. Cuando los argumentos ya están en los registros correctos, no es necesario moverlos.
Mientras leía el código desensamblado, he visto algunos registros ridículos barajando porque la gente no siguió esta regla.
fuente
Dos técnicas de codificación que no vi en la lista anterior:
Omita el enlazador escribiendo código como fuente única
Si bien la compilación separada es realmente buena para el tiempo de compilación, es muy mala cuando se habla de optimización. Básicamente, el compilador no puede optimizar más allá de la unidad de compilación, es decir, el dominio reservado del enlazador.
Pero si diseña bien su programa, también puede compilarlo a través de una fuente común única. Es decir, en lugar de compilar unit1.cy unit2.c, luego vincular ambos objetos, compilar all.c que simplemente #incluye unit1.cy unit2.c. Por lo tanto, se beneficiará de todas las optimizaciones del compilador.
Es muy parecido a escribir programas de solo encabezados en C ++ (e incluso más fácil de hacer en C).
Esta técnica es bastante fácil si escribe su programa para habilitarla desde el principio, pero también debe tener en cuenta que cambia parte de la semántica de C y puede encontrar algunos problemas como variables estáticas o colisión de macros. Para la mayoría de los programas, es bastante fácil superar los pequeños problemas que se presentan. También tenga en cuenta que la compilación como fuente única es mucho más lenta y puede requerir una gran cantidad de memoria (por lo general, no es un problema con los sistemas modernos).
¡Usando esta técnica simple, hice algunos programas que escribí diez veces más rápido!
Al igual que la palabra clave register, este truco también podría quedar obsoleto pronto. La optimización a través del enlazador comienza a ser compatible con los compiladores gcc: Optimización del tiempo de enlace .
Separe las tareas atómicas en bucles
Este es más complicado. Se trata de la interacción entre el diseño de algoritmos y la forma en que el optimizador administra la caché y la asignación de registros. Muy a menudo, los programas tienen que recorrer una estructura de datos y realizar algunas acciones para cada elemento. Muy a menudo, las acciones realizadas se pueden dividir entre dos tareas lógicamente independientes. Si ese es el caso, puede escribir exactamente el mismo programa con dos bucles en el mismo límite realizando exactamente una tarea. En algunos casos, escribirlo de esta manera puede ser más rápido que el bucle único (los detalles son más complejos, pero una explicación puede ser que con el caso de tarea simple todas las variables se pueden mantener en los registros del procesador y con el más complejo no es posible y algunos los registros deben escribirse en la memoria y leerse más tarde y el costo es más alto que el control de flujo adicional).
Tenga cuidado con este (actuaciones de perfil con este truco o no), ya que, al igual que con el registro, también puede dar resultados inferiores a los mejorados.
fuente
De hecho, he visto esto hecho en SQLite y afirman que da como resultado un aumento de rendimiento de ~ 5%: coloque todo su código en un archivo o use el preprocesador para hacer el equivalente a esto. De esta forma, el optimizador tendrá acceso a todo el programa y podrá realizar más optimizaciones entre procedimientos.
fuente
-O3
, eliminó el 22% del tamaño original de mi programa. (No depende de la CPU, por lo que no tengo mucho que decir sobre la velocidad).La mayoría de los compiladores modernos deberían hacer un buen trabajo acelerando la recursividad de la cola , porque las llamadas a funciones pueden optimizarse.
Ejemplo:
Por supuesto, este ejemplo no tiene ninguna comprobación de límites.
Edición tardía
Si bien no tengo conocimiento directo del código; Parece claro que los requisitos de uso de CTE en SQL Server se diseñaron específicamente para que pueda optimizar a través de la recursividad final.
fuente
¡No hagas el mismo trabajo una y otra vez!
Un antipatrón común que veo va en estas líneas:
El compilador tiene que llamar a todas esas funciones todo el tiempo. Suponiendo que usted, el programador, sepa que el objeto agregado no cambia en el transcurso de estas llamadas, por el amor de todo lo que es santo ...
En el caso del getter singleton, las llamadas pueden no ser demasiado costosas, pero ciertamente es un costo (por lo general, "verifique si el objeto ha sido creado, si no lo ha creado, créelo y luego devuélvalo). más complicada se vuelve esta cadena de captadores, más tiempo perdido tendremos.
fuente
Utilice el ámbito más local posible para todas las declaraciones de variables.
Usar
const
siempre que sea posibleNo use el registro a menos que planee crear un perfil con y sin él
Los 2 primeros, especialmente el 1, ayudan al optimizador a analizar el código. Le ayudará especialmente a tomar buenas decisiones sobre qué variables mantener en los registros.
Usar a ciegas la palabra clave register es tan probable que ayude como que perjudique su optimización. Es demasiado difícil saber qué importará hasta que observe el resultado o el perfil del ensamblaje.
Hay otras cosas que son importantes para obtener un buen rendimiento del código; diseñar sus estructuras de datos para maximizar la coherencia de la caché, por ejemplo. Pero la pregunta era sobre el optimizador.
fuente
Alinee sus datos con los límites nativos / naturales.
fuente
Me acordé de algo que encontré una vez, donde el síntoma era simplemente que nos estábamos quedando sin memoria, pero el resultado fue un rendimiento sustancialmente mayor (así como enormes reducciones en la huella de memoria).
El problema en este caso fue que el software que estábamos usando hizo toneladas de pequeñas asignaciones. Por ejemplo, asignar cuatro bytes aquí, seis bytes allí, etc. También hay muchos objetos pequeños que se ejecutan en el rango de 8-12 bytes. El problema no era tanto que el programa necesitaba muchas cosas pequeñas, es que asignaba muchas cosas pequeñas individualmente, lo que infló cada asignación a (en esta plataforma en particular) 32 bytes.
Parte de la solución fue armar un grupo de objetos pequeños al estilo Alexandrescu, pero extenderlo para poder asignar matrices de objetos pequeños así como elementos individuales. Esto también ayudó enormemente en el rendimiento, ya que caben más elementos en la caché a la vez.
La otra parte de la solución fue reemplazar el uso desenfrenado de miembros char * administrados manualmente con una cadena SSO (optimización de cadena pequeña). La asignación mínima es de 32 bytes, construí una clase de cadena que tenía un búfer de 28 caracteres integrado detrás de un char *, por lo que el 95% de nuestras cadenas no necesitaban hacer una asignación adicional (y luego reemplacé manualmente casi todas las apariencias de char * en esta biblioteca con esta nueva clase, eso fue divertido o no). Esto también ayudó mucho con la fragmentación de la memoria, lo que luego aumentó la localidad de referencia para otros objetos apuntados, y de manera similar, hubo ganancias de rendimiento.
fuente
Una técnica ordenada que aprendí del comentario de @MSalters sobre esta respuesta permite a los compiladores copiar la elisión incluso cuando devuelven diferentes objetos de acuerdo con alguna condición:
fuente
Si tiene pequeñas funciones a las que llama repetidamente, en el pasado obtuve grandes ganancias al colocarlas en encabezados como "estáticas en línea". Las llamadas a funciones en el ix86 son sorprendentemente caras.
Reimplementar funciones recursivas de una manera no recursiva usando una pila explícita también puede ganar mucho, pero entonces realmente estás en el ámbito del tiempo de desarrollo frente a la ganancia.
fuente
Este es mi segundo consejo de optimización. Al igual que con mi primer consejo, esto es de uso general, no específico del idioma o procesador.
Lea detenidamente el manual del compilador y comprenda lo que le dice. Utilice el compilador al máximo.
Estoy de acuerdo con uno o dos de los otros encuestados que han identificado la selección del algoritmo adecuado como fundamental para exprimir el rendimiento de un programa. Más allá de eso, la tasa de rendimiento (medida en la mejora de la ejecución del código) en el tiempo que invierte en usar el compilador es mucho más alta que la tasa de rendimiento en ajustar el código.
Sí, los escritores de compiladores no son de una raza de gigantes de la codificación y los compiladores contienen errores y lo que debería, según el manual y según la teoría del compilador, hacer que las cosas sean más rápidas, a veces las hace más lentas. Es por eso que debe dar un paso a la vez y medir el rendimiento antes y después de los ajustes.
Y sí, en última instancia, es posible que se enfrente a una explosión combinatoria de indicadores del compilador, por lo que debe tener uno o dos scripts para ejecutar make con varios indicadores del compilador, poner en cola los trabajos en el clúster grande y recopilar las estadísticas de tiempo de ejecución. Si solo está usted y Visual Studio en una PC, se quedará sin interés mucho antes de haber probado suficientes combinaciones de suficientes indicadores del compilador.
Saludos
marca
Cuando tomo por primera vez un fragmento de código, generalmente puedo obtener un factor de 1.4 - 2.0 veces más rendimiento (es decir, la nueva versión del código se ejecuta en 1 / 1.4 o 1/2 del tiempo de la versión anterior) dentro de un día o dos jugando con las banderas del compilador. Por supuesto, eso puede ser un comentario sobre la falta de conocimiento de los compiladores entre los científicos que originan gran parte del código en el que trabajo, más que un síntoma de mi excelencia. Habiendo configurado los indicadores del compilador al máximo (y rara vez es solo -O3), puede llevar meses de arduo trabajo obtener otro factor de 1.05 o 1.1
fuente
Cuando DEC salió con sus procesadores alfa, hubo una recomendación de mantener el número de argumentos de una función por debajo de 7, ya que el compilador siempre intentaría poner hasta 6 argumentos en los registros automáticamente.
fuente
Para el rendimiento, concéntrese primero en escribir código que se pueda mantener: en componentes, poco acoplado, etc., de modo que cuando tenga que aislar una parte para reescribirla, optimizarla o simplemente perfilarla, puede hacerlo sin mucho esfuerzo.
Optimizer ayudará marginalmente al rendimiento de su programa.
fuente
Aquí está obteniendo buenas respuestas, pero ellos asumen que su programa está bastante cerca del óptimo para empezar, y usted dice
En mi experiencia, un programa puede estar escrito correctamente, pero eso no significa que sea casi óptimo. Se necesita un trabajo extra para llegar a ese punto.
Si puedo dar un ejemplo, esta respuesta muestra cómo un programa de apariencia perfectamente razonable se hizo 40 veces más rápido mediante la macrooptimización . No se pueden hacer grandes aceleraciones en todos los programas como se escribieron por primera vez, pero en muchos (excepto en programas muy pequeños), en mi experiencia, se puede hacer.
Una vez hecho esto, la microoptimización (de los puntos calientes) puede brindarle una buena recompensa.
fuente
Yo uso el compilador de Intel. tanto en Windows como en Linux.
cuando más o menos termino perfilo el código. luego cuelgue de los puntos de acceso e intente cambiar el código para permitir que el compilador haga un mejor trabajo.
si un código es computacional y contiene muchos bucles (el informe de vectorización en el compilador de Intel es muy útil), busque 'vec-report' en la ayuda.
entonces la idea principal - pulir el código crítico de rendimiento. en cuanto al resto - prioridad para ser correcto y mantenible - funciones cortas, código claro que podría entenderse 1 año después.
fuente
Una optimización que he usado en C ++ es crear un constructor que no hace nada. Uno debe llamar manualmente a un init () para poner el objeto en un estado de trabajo.
Esto tiene ventajas en el caso de que necesite un gran vector de estas clases.
Llamo a reserve () para asignar el espacio para el vector, pero el constructor en realidad no toca la página de memoria en la que se encuentra el objeto. Así que he gastado algo de espacio de direcciones, pero en realidad no he consumido mucha memoria física. Evito las fallas de página asociadas a los costos de construcción asociados.
A medida que genero objetos para llenar el vector, los configuro usando init (). Esto limita el total de fallas de mi página y evita la necesidad de cambiar el tamaño () del vector mientras lo llena.
fuente
Una cosa que he hecho es tratar de mantener las acciones costosas en lugares donde el usuario podría esperar que el programa se retrase un poco. El rendimiento general está relacionado con la capacidad de respuesta, pero no es lo mismo, y para muchas cosas, la capacidad de respuesta es la parte más importante del rendimiento.
La última vez que realmente tuve que hacer mejoras en el rendimiento general, estuve atento a los algoritmos subóptimos y busqué lugares que probablemente tuvieran problemas de caché. Primero hice un perfil y medí el desempeño, y nuevamente después de cada cambio. Luego la empresa colapsó, pero de todos modos fue un trabajo interesante e instructivo.
fuente
Durante mucho tiempo sospeché, pero nunca probé, que declarar matrices para que tengan una potencia de 2, como el número de elementos, permite al optimizador hacer una reducción de fuerza reemplazando una multiplicación por un desplazamiento por un número de bits, al mirar hacia arriba elementos individuales.
fuente
val * 7
convertido en lo que de otro modo se vería(val << 3) - val
.Coloque funciones pequeñas y / o llamadas con frecuencia en la parte superior del archivo fuente. Eso hace que sea más fácil para el compilador encontrar oportunidades para la inserción.
fuente