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

116

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?

[Editar] Enlace relacionado con la compensación

EvilTeach
fuente
7
Podría ser un buen candidato para la wiki de la comunidad en mi humilde opinión, ya que no hay una respuesta definitiva 'única' a esta pregunta (interesante) ...
ChristopheD
Lo extraño todo el tiempo. Gracias por señalarlo.
EvilTeach
¿Por "mejor" te refieres simplemente a "más rápido" o tienes otros criterios de excelencia en mente?
Marca de alto rendimiento
1
Es bastante difícil escribir un buen asignador de registros, especialmente portátil, y la asignación de registros es absolutamente esencial para el rendimiento y el tamaño del código. registerde hecho, hizo que el código sensible al rendimiento fuera más portátil al combatir compiladores deficientes.
Potatoswatter
1
@EvilTeach: wiki de comunidad no significa "sin respuesta definitiva", no es sinónimo de etiqueta subjetiva. Wiki de la comunidad significa que desea entregar su publicación a la comunidad para que otras personas puedan editarla. No se sienta presionado a escribir en wiki sus preguntas si no tiene ganas.
Julieta

Respuestas:

54

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

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 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:

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 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).

celion
fuente
7
Esto tiene el beneficio adicional de hacer que las cosas sean más fáciles de leer / entender para los programadores, ya que tampoco tienen que preocuparse por posibles efectos secundarios no obvios.
Michael Burr
La mayoría de los IDE muestran variables locales de forma predeterminada, por lo que hay menos escritura
EvilTeach
9
también puede habilitar esa optimización mediante el uso de punteros restringidos
Ben Voigt
4
@Ben: eso es cierto, pero creo que de esta manera es más clara. Además, si la entrada y la salida se superpusieron, creo que el resultado no está especificado con punteros restringidos (probablemente obtenga un comportamiento diferente entre la depuración y la liberación), mientras que de esta manera al menos será consistente. No me malinterpretes, me gusta usar restringir, pero me gusta no necesitarlo aún más.
celion
Solo tiene que esperar que Foo no tenga definida una operación de copia que copie un par de megas de datos ;-)
Skizz
76

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.

Marca de alto rendimiento
fuente
20
Los desarrolladores más complejos tienen un tiempo limitado, como todos los demás. No todas las optimizaciones se incluirán en el compilador. Me gusta &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.
Dave Jarvis
22
& y% casi siempre está optimizado, junto con la mayoría de los otros trucos aritméticos baratos como gratuitos. Lo que no se optimiza es el caso de que el operando de la derecha sea una variable que siempre es una potencia de dos.
Potatoswatter
8
Para aclarar, parece haber confundido a algunos lectores: el consejo en la práctica de codificación que propongo es desarrollar primero un código sencillo que no haga uso de instrucciones de diseño de memoria para establecer una línea de base de rendimiento. Luego, pruebe las cosas una por una y mida su impacto. No he ofrecido ningún consejo sobre la realización de operaciones.
Marca de alto rendimiento
17
Para poder constantes-de-dos n, reemplaza gcc % ncon & (n-1) incluso cuando se desactiva la optimización . Eso no es exactamente "rara vez, si es que alguna vez" ...
Porculus
12
% NO PUEDE optimizarse como & cuando el tipo está firmado, debido a las estúpidas reglas de C para la división de enteros negativos (redondear hacia 0 y tener un resto negativo, en lugar de redondear hacia abajo y tener siempre un resto positivo). Y la mayoría de las veces, los codificadores ignorantes utilizar tipos firmados ...
R .. GitHub dejar de ayudar a ICE
47

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:

#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];
  }
}
vicatcu
fuente
Estrictamente hablando, esto no es un problema de optimización, sino un problema de optimización.
EvilTeach
10
Seguro que es un problema de optimizador. La gente ha estado escribiendo artículos sobre la optimización del intercambio automático de bucles durante décadas.
Phil Miller
20
@Potatoswatter ¿De qué estás hablando? El compilador de C puede hacer lo que quiera siempre que se observe el mismo resultado final, y de hecho GCC 4.4 tiene lo -floop-interchangeque cambiará un bucle interno y externo si el optimizador lo considera rentable.
ephemient
2
Eh, bueno, ahí tienes. La semántica de C a menudo se ve empañada por problemas de alias. ¡Supongo que el verdadero consejo aquí es pasar esa bandera!
Potatoswatter
36

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 inlinemacros

Cada 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 #definemacros o inlinefunciones. (Sí, sé que inlinees 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:

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 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 memcpyfue mucho más rápido que el del compilador memcpy. - TM

Reducción de ifdeclaraciones

Los 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:

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 statuses 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;
};

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:

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 printf familia para datos constantes

Los 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 charmatriz usando multiple sprintf, luego use fwrite. 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 el staticprefijo.

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.

Thomas Matthews
fuente
2
Es interesante que pueda proporcionar un ejemplo en el que obtuvo un mejor código con algunas declaraciones pequeñas, en lugar de una más grande. ¿Puede mostrar un ejemplo de cómo reescribir un if, usando booleanos. Generalmente, dejaría que el compilador se desenrolle el ciclo, ya que probablemente tenga una mejor idea del tamaño de la caché. Estoy un poco sorprendido con la idea de correr y luego escribir. Creo que fprintf en realidad lo hace bajo el capó. ¿Puedes darnos un poco más de detalle aquí?
EvilTeach
1
No hay garantía de que los fprintfformatos en un búfer separado luego generen el búfer. Un fprintfformato 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.
Thomas Matthews
3
Una vez obtuve una mejora significativa en el rendimiento al enrollar un bucle. Luego descubrí cómo enrollarlo más apretado usando alguna indirección, y el programa se volvió notablemente más rápido. (La creación de perfiles mostró que esta función en particular representaba el 60-80% del tiempo de ejecución, y probé el rendimiento cuidadosamente antes y después). Creo que la mejora se debió a una mejor localidad, pero no estoy completamente seguro de eso.
David Thornley
16
Muchas de estas son optimizaciones del programador en lugar de formas para que los programadores ayuden al compilador a la optimización, que era la idea central de la pregunta original. Por ejemplo, desenrollado de bucle. Sí, puede hacer su propio desenrollado, pero creo que es más interesante averiguar qué obstáculos existen para que el compilador se desenrolle y elimine esos obstáculos.
Adrian McCarthy
26

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.

Potatoswatter
fuente
14
Esta es una buena respuesta, pero tenga en cuenta que la optimización de todo el programa se está volviendo más popular y, de hecho, puede funcionar en línea en todas las unidades de traducción.
Phil Miller
1
@Novelocrat Sí, no hace falta decir que me sorprendió mucho la primera vez que vi algo de A.cinsertarse en B.c.
Jonathon Reinhart
18

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. .

Francis Boivin
fuente
4
+1 para load-hit-stores y diferentes tipos de registros. No estoy seguro de qué tan importante es en x86, pero están demorando en PowerPC (por ejemplo, Xbox360 y Playstation3).
Celion
La mayoría de los artículos sobre técnicas de optimización de bucles del compilador asumen un anidamiento perfecto, lo que significa que el cuerpo de cada bucle, excepto el más interno, es solo otro bucle. Estos artículos simplemente no discuten los pasos necesarios para generalizarlos, incluso si está muy claro que pueden serlo. Por lo tanto, esperaría que muchas implementaciones no admitan realmente esas generalizaciones, debido al esfuerzo adicional que implica. Por lo tanto, los muchos algoritmos para optimizar el uso de la caché en bucles podrían funcionar mucho mejor en nidos perfectos que en nidos imperfectos.
Phil Miller
11

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.

anon
fuente
7
Confieso que soy peculiar: trabajo en grandes códigos científicos de procesamiento de números que están limitados al ancho de banda de la memoria. Para la población general de programas, estoy de acuerdo con Neil.
Marca de alto rendimiento
6
Cierto; pero una gran parte de ese código ligado a E / S hoy en día está escrito en lenguajes que son prácticamente pesimizadores , lenguajes que ni siquiera tienen compiladores. Sospecho que las áreas donde todavía se usan C y C ++ tenderán a ser áreas donde es más importante optimizar algo (uso de CPU, uso de memoria, tamaño del código ...)
Porculus
3
He pasado la mayor parte de los últimos 30 años trabajando en código con muy poca E / S. Ahorre 2 años haciendo bases de datos. Gráficos, sistemas de control, simulación, ninguno de ellos ligado a E / S. Si la E / S fuera el cuello de botella de la mayoría de las personas, no le prestaríamos mucha atención a Intel y AMD.
phkahler
2
Sí, realmente no compro este argumento; de lo contrario, nosotros (en mi trabajo) no estaríamos buscando formas de pasar más tiempo de cálculo también haciendo E / S. Además, gran parte del software vinculado a E / S con el que me he encontrado ha estado vinculado a E / S porque la E / S se realizó de manera descuidada; si se optimizan los patrones de acceso (al igual que con la memoria), se pueden obtener enormes ganancias en el rendimiento.
dash-tom-bang
3
Recientemente descubrí que casi ningún código escrito en el lenguaje C ++ está ligado a E / S. Claro, si está llamando a una función del sistema operativo para la transferencia masiva de disco, su hilo puede entrar en espera de E / S (pero con el almacenamiento en caché, incluso eso es cuestionable). Pero las funciones habituales de la biblioteca de E / S, las que todos recomiendan porque son estándar y portátiles, en realidad son miserablemente lentas en comparación con la tecnología de disco moderna (incluso las cosas de precio moderado). Lo más probable es que la E / S sea el cuello de botella solo si va a vaciar todo el camino hasta el disco después de escribir solo unos pocos bytes. OTOH, UI es un asunto diferente, los humanos somos lentos.
Ben Voigt
11

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:

  • usar listas de inicialización del constructor
  • usar operadores de prefijo
  • usar constructores explícitos
  • funciones en línea
  • evitar objetos temporales
  • ser consciente del costo de las funciones virtuales
  • devolver objetos a través de parámetros de referencia
  • considerar la asignación por clase
  • considere los asignadores de contenedores stl
  • la optimización del 'miembro vacío'
  • etc
Toad
fuente
8
No mucho, rara vez. Sin embargo, mejora la corrección real.
Potatoswatter
5
En C y C ++, el compilador no puede usar const para optimizar porque desecharlo es un comportamiento bien definido.
dsimcha
+1: const es un buen ejemplo de algo que afectará directamente al código compilado. re @ dsimcha's comment - un buen compilador probará para ver si esto sucede. Por supuesto, un buen compilador "encontrará" elementos const que no están declarados de esa manera de todos modos ...
Hogan
@dsimcha: Sin embargo, cambiar un puntero calificado const y restrict no está definido. Entonces, un compilador podría optimizar de manera diferente en tal caso.
Dietrich Epp
6
@dsimcha descartar constuna constreferencia o un constpuntero a un no constobjeto está bien definido. modificar un constobjeto real (es decir, uno declarado como constoriginalmente) no lo es.
Stephen Lin
9

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:

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

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.

#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 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:

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 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.

nategoose
fuente
2
En realidad, no es necesario usar la resta cuando se prueban rangos, LLVM, GCC y mi compilador al menos lo hacen automáticamente. Pocas personas probablemente entenderían lo que hace el código con resta y menos aún por qué funciona realmente.
Gratian Lup
en el ejemplo anterior, no se puede llamar a b () porque si (x <0) entonces se llamará a ().
EvilTeach
@EvilTeach No, no lo hará. La comparación que da como resultado la llamada a a () es! X
nategoose
@nategoose. si x es -3 entonces! x es verdadero.
EvilTeach
@EvilTeach en C 0 es falso y todo lo demás es verdadero, entonces -3 es verdadero, ¡entonces! -3 es falso
nategoose
9

Escribí un compilador C optimizado y aquí hay algunas cosas muy útiles a considerar:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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).

  8. 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 .

Gratian Lup
fuente
7

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.

figurassa
fuente
Mi regla general es que si tiene que asignar dinámicamente, obtenga una matriz para no tener que volver a hacerlo. Preasigne los vectores.
EvilTeach
7

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.

Zan Lynx
fuente
2
Ni C ni C ++ ofrecen garantías sobre, ni siquiera mencionan, el paso de registros o ubicaciones de pila particulares. Es la ABI (por ejemplo, Linux ELF) la que determina los detalles del paso de parámetros.
Emmet
5

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.

kriss
fuente
2
Sí, por ahora, LTO ha hecho que la primera mitad de esta publicación sea redundante y probablemente un mal consejo.
underscore_d
@underscore_d: todavía hay algunos problemas (principalmente relacionados con la visibilidad de los símbolos exportados), pero desde el punto de vista del mero rendimiento, probablemente ya no haya más.
kriss
4

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.

dsimcha
fuente
5
Poner las funciones que se utilizan juntas en estrecha proximidad física en la fuente aumenta la probabilidad de que estén cerca unas de otras en archivos de objetos y cerca de otras en su ejecutable. Esta localidad mejorada de instrucciones puede ayudar a evitar errores de caché de instrucciones durante la ejecución.
paxos1977
El compilador AIX tiene un conmutador de compilador para fomentar ese comportamiento -qipa [= <suboptions_list>] | -qnoipa Activa o personaliza una clase de optimizaciones conocidas como análisis entre procedimientos (IPA).
EvilTeach
4
Lo mejor es tener una forma de desarrollo que no requiera esto. Usar este hecho como una excusa para escribir código no modular resultará en un código lento y con problemas de mantenimiento.
Hogan
3
Creo que esta información está un poco anticuada. En teoría, las funciones de optimización de todo el programa integradas en muchos compiladores ahora (por ejemplo, "Optimización de tiempo de enlace" en gcc) permiten los mismos beneficios, pero con un flujo de trabajo totalmente estándar (además de tiempos de recompilación más rápidos que ponerlo todo en un solo archivo !)
Ponkadoodle
@Wallacoloo Sin duda, esto está muy desactualizado. FWIW, acabo de usar LTO de GCC por primera vez hoy y, en igualdad de condiciones -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).
underscore_d
4

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:

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);
}

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.

Hogan
fuente
1
la pregunta es sobre C. C no elimina la recursividad de cola, por lo que la recursividad de cola u otra, la pila podría explotar si la recursividad es demasiado profunda.
Sapo
1
He evitado el problema de la convención de llamadas utilizando un goto. De esa manera hay menos gastos generales.
EvilTeach
2
@hogan: esto es nuevo para mí. ¿Podría señalar algún compilador que haga esto? ¿Y cómo puede estar seguro de que realmente lo optimiza? Si pudiera hacer esto, realmente necesita estar seguro de que lo hace. No es algo que espere que el optimizador del compilador capte (como la inserción que puede o no funcionar)
Toad
6
@hogan: Me quedo corregido. Tiene razón en que Gcc y MSVC hacen optimización de recursión de cola.
Toad
5
Este ejemplo no es una recursividad de cola, ya que no es la llamada recursiva que es la última, es la multiplicación.
Brian Young
4

¡No hagas el mismo trabajo una y otra vez!

Un antipatrón común que veo va en estas líneas:

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

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 ...

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

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.

dash-tom-bang
fuente
3
  1. Utilice el ámbito más local posible para todas las declaraciones de variables.

  2. Usar constsiempre que sea posible

  3. No 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.

John Knoeller
fuente
3

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.

dash-tom-bang
fuente
3

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:

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

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;
Xeo
fuente
2

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.

Remy
fuente
La conversión de la recursividad en una pila es una optimización asumida en ompf.org, para las personas que desarrollan raytracers y escriben otros algoritmos de renderizado.
Tom
... Debo agregar a esto, que la mayor sobrecarga en mi proyecto personal de raytracer es la recursividad basada en vtable a través de una jerarquía de volumen delimitador usando el patrón compuesto. En realidad, es solo un montón de cajas anidadas estructuradas como un árbol, pero el uso del patrón provoca un aumento de datos (punteros de tabla virtual) y reduce la coherencia de las instrucciones (lo que podría ser un bucle pequeño / ajustado ahora es una cadena de llamadas a funciones)
Tom
2

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

Marca de alto rendimiento
fuente
2

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.

EvilTeach
fuente
x86-64 bit también permiten una gran cantidad de parámetros pasados ​​por registros, lo que puede tener un efecto dramático en la sobrecarga de llamadas a funciones.
Tom
1

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.

Ariel
fuente
3
Eso solo funciona si las "interfaces" de acoplamiento en sí mismas se pueden optimizar. Una interfaz puede ser intrínsecamente "lenta", por ejemplo, forzando búsquedas o cálculos redundantes, o forzando un acceso incorrecto a la caché.
Tom
1

Aquí está obteniendo buenas respuestas, pero ellos asumen que su programa está bastante cerca del óptimo para empezar, y usted dice

Suponga que el programa se ha escrito correctamente, compilado con optimización completa, probado y puesto en producción.

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.

Mike Dunlavey
fuente
1

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.

jf.
fuente
Se está acercando a responder la pregunta ... ¿qué tipo de cosas le hace al código para que sea posible que el compilador haga ese tipo de optimizaciones?
EvilTeach
1
Tratando de escribir más en estilo C (en lugar de en C ++), por ejemplo, evitando funciones virtuales sin necesidad absoluta, especialmente si se van a llamar con frecuencia, evite AddRefs ... y todas las cosas interesantes (nuevamente a menos que realmente sea necesario). Escriba código fácil para insertar: menos parámetros, menos "if" -s. No utilice variables globales a menos que sea absolutamente necesario. En la estructura de datos, coloque los campos más anchos primero (double, int64 va antes de int), de modo que el compilador alinee la estructura en el tamaño natural del primer campo, alineando bien para perf.
jf.
1
El diseño y el acceso a los datos son absolutamente críticos para el rendimiento. Entonces, después de la creación de perfiles, a veces divido una estructura en varias siguiendo la localidad de los accesos. Un truco más general: use int o size-t frente a char, incluso los valores de datos son pequeños, evite varios perf. penalizaciones almacén a bloqueo de carga, problemas con paradas de registros parciales. por supuesto, esto no se aplica cuando se necesitan grandes conjuntos de datos.
jf.
Una más: evite las llamadas al sistema, a menos que exista una necesidad real :) - son MUY caras
jf.
2
@jf: Hice +1 en tu respuesta, pero ¿podrías mover la respuesta de los comentarios al cuerpo de la respuesta? Será más fácil de leer.
kriss
1

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.

EvilTeach
fuente
6
Creo que una implementación típica de std :: vector en realidad no construye más objetos cuando reserva () más capacidad. Simplemente asigna páginas. Los constructores se llaman más tarde, usando la ubicación new, cuando en realidad agrega objetos al vector, que es (presumiblemente) justo antes de llamar a init (), por lo que realmente no necesita la función init () separada. También recuerde que incluso si su constructor está "vacío" en el código fuente, el constructor compilado puede contener código para inicializar cosas como tablas virtuales y RTTI, por lo que las páginas se tocan en el momento de la construcción de todos modos.
Wyzard
1
Sí. En nuestro caso usamos push_back para poblar el vector. Los objetos no tienen funciones virtuales, por lo que no es un problema. La primera vez que lo probamos con el constructor, nos sorprendió el volumen de fallas de página. Me di cuenta de lo que sucedió, le arrancamos las tripas al constructor y el problema de fallas de página desapareció.
EvilTeach
Eso me sorprende bastante. ¿Qué implementaciones de C ++ y STL estaba usando?
David Thornley
3
Estoy de acuerdo con los demás, esto suena como una mala implementación de std :: vector. Incluso si sus objetos tuvieran vtables, no se construirían hasta su push_back. Debería poder probar esto declarando que el constructor predeterminado es privado, porque todo el vector que necesitará es el constructor de copia para push_back.
Tom
1
@David: la implementación se realizó en AIX.
EvilTeach
1

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.

David Thornley
fuente
0

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.

EvilTeach
fuente
6
Eso solía ser cierto, hoy en día ya lo es. De hecho, ocurre exactamente lo contrario. Si declara sus matrices con potencias de dos, muy probablemente se encontrará con la situación de que trabaja en dos punteros separados por una potencia de dos en la memoria. El problema es que los cachés de la CPU están organizados así y puede terminar con los dos arreglos peleando alrededor de una línea de caché. Obtienes un rendimiento horrible de esa manera. Tener uno de los punteros un par de bytes por delante (por ejemplo, no poder de dos) evita esta situación.
Nils Pipenbrinck
+1 Nils, y una ocurrencia específica de esto es "alias de 64k" en hardware Intel.
Tom
Esto es algo que se refuta fácilmente mirando el desmontaje, por cierto. Me sorprendió, hace años, ver cómo gcc optimizaba todo tipo de multiplicaciones constantes con cambios y sumas. Por ejemplo, val * 7convertido en lo que de otro modo se vería (val << 3) - val.
dash-tom-bang
0

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.

Mark Ransom
fuente
De Verdad? ¿Puede citar una justificación y ejemplos para esto? No digo que sea falso, solo suena poco intuitivo que la ubicación importaría.
underscore_d
@underscore_d no puede insertar algo hasta que se conozca la definición de la función. Si bien los compiladores modernos pueden realizar varias pasadas para que la definición se conozca en el momento de la generación del código, no lo asumo.
Mark Ransom
Supuse que los compiladores funcionan con gráficos de llamadas abstractos en lugar de un orden de función física, lo que significa que esto no importaría. Claro, supongo que no está de más tener mucho cuidado, especialmente cuando, dejando de lado el rendimiento, en mi opinión, parece más lógico definir funciones que se llaman antes que aquellas que las llaman. Tendría que probar el rendimiento, pero me sorprendería si importara, pero hasta entonces, ¡estoy abierto a que me sorprendan!
underscore_d