En C ++, ¿debería molestarme en almacenar en caché las variables o dejar que el compilador haga la optimización? (Aliasing)

114

Considere el siguiente código ( pes de tipo unsigned char*y bitmap->widthes de algún tipo entero, exactamente que se desconoce y depende de la versión de alguna biblioteca externa que estemos usando):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

¿Vale la pena optimizarlo [..]

¿Podría haber un caso en el que esto pudiera producir resultados más eficientes escribiendo:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... o es esto trivial para que el compilador lo optimice?

¿Qué consideraría un código "mejor"?

Nota del editor (Ike): para aquellos que se preguntan sobre el texto tachado, la pregunta original, tal como está redactada, estaba peligrosamente cerca de un territorio fuera del tema y estaba muy cerca de cerrarse a pesar de los comentarios positivos. Estos han sido eliminados. Sin embargo, no castigue a quienes respondieron que abordaron estas secciones afectadas de la pregunta.

Yaron Cohen-Tal
fuente
19
Si *pes del mismo tipo que widthentonces, no es trivial optimizarlo, ya que ppodría apuntar widthy modificarlo dentro del ciclo.
emlai
31
Preguntar si el compilador optimiza una operación en particular suele ser una pregunta incorrecta. Lo que en última instancia le interesa (normalmente) es qué versión se ejecuta más rápido, qué debería medir simplemente.
SirGuy
4
@GuyGreer Estoy de acuerdo, aunque diría que la pregunta es buena, o al menos interesante, aunque desafortunadamente la respuesta es "tienes que medirlo, por caso de uso". La razón es que la funcionalidad es portátil, pero el rendimiento no. Entonces, en realidad, depende de cada parte del proceso de compilación, comenzando en el compilador y terminando en el sitio de destino (combinación de sistema operativo / hardware). Y, por supuesto, la mejor suposición es que el compilador es más inteligente que el humano en esto.
luk32
19
Si fuera un compilador, vería que sus dos ejemplos no son iguales. Es posible que papunte a la misma memoria que bitmap->width. Por lo tanto, no puedo optimizar legalmente el primer ejemplo al segundo.
Mysticial
4
¿Dónde se almacena "p"? Sugeriría que podría obtener una ganancia de rendimiento realmente enorme haciendo algo como "char * restrict p2 = p;" y luego usar "p2" en lugar de "p" dentro de su ciclo. Luego, si desea que los cambios a "p2" se apliquen de nuevo ap, use "p + = (p2-p);". Tenga en cuenta que ningún puntero escrito dentro de la vida de p2 por un puntero no copiado de p2 puede leerse usando un puntero copiado de p2, ni viceversa, y ninguna copia de p2 puede usarse para ningún propósito después de la vida de p2, pero un compilador puede usarlos. hechos para permitir optimizaciones que no se pueden lograr por ningún otro medio.
supercat

Respuestas:

81

A primera vista, pensé que el compilador podría generar un ensamblado equivalente para ambas versiones con los indicadores de optimización activados. Cuando lo revisé, me sorprendió ver el resultado:

Fuente unoptimized.cpp

nota: este código no debe ejecutarse.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Fuente optimized.cpp

nota: este código no debe ejecutarse.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Compilacion

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Montaje (no optimizado)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Montaje (optimizado.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

diff

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

El ensamblado generado para la versión optimizada realmente carga ( lea) la widthconstante a diferencia de la versión no optimizada que calcula el widthdesplazamiento en cada iteración ( movq).

Cuando tenga tiempo, eventualmente publico algún punto de referencia sobre eso. Buena pregunta.

YSC
fuente
3
Sería interesante ver si el código se generó de manera diferente si se lanza a en const unsignedlugar de solo unsigneden el caso no optimizado.
Mark Ransom
2
@MarkRansom Supongo que no debería hacer una diferencia: la "promesa" de ser constante es solo durante la comparación única, no para todo el ciclo
Hagen von Eitzen
13
Por favor, NUNCA utilizar la función mainde prueba para una optimización. Gcc lo marca a propósito como frío y, por lo tanto, desactiva algunas optimizaciones para él. No sé si ese es el caso aquí, pero es un hábito importante a tener.
Marc Glisse
3
@MarcGlisse Tienes toda la razón. Lo escribí con prisa, lo mejoraré.
YSC
3
Aquí hay un enlace a ambas funciones en una unidad de compilación en godbolt , asumiendo que bitmapes global. La versión que no es CSEd usa un operando de memoria para cmp, que no es un problema para perf en este caso. Si fuera local, el compilador podría asumir que otros punteros no podrían "conocerlo" y señalarlo. No es una mala idea almacenar expresiones que involucren globales en variables temporales, siempre que mejore (o no perjudique) la legibilidad, o si el rendimiento es crítico. A menos que estén sucediendo muchas cosas, estos lugareños generalmente pueden vivir en registros y nunca se derramarán.
Peter Cordes
38

En realidad, no hay información suficiente de su fragmento de código para poder decirlo, y lo único en lo que puedo pensar es en el alias. Desde nuestro punto de vista, es bastante claro que no desea py bitmapque apuntan a la misma ubicación en la memoria, pero el compilador no sabe que y (porque pes de tipo char*) el compilador tiene que hacer este trabajo código incluso si py bitmapsuperposición.

Esto significa en este caso que si el bucle cambia a bitmap->widthtravés del puntero, pentonces debe verse cuando se vuelva a leer bitmap->widthmás adelante, lo que a su vez significa que almacenarlo en una variable local sería ilegal.

Dicho esto, creo que algunos compiladores generarán a veces dos versiones del mismo código (he visto evidencia circunstancial de esto, pero nunca busqué directamente información sobre lo que está haciendo el compilador en este caso), y verificará rápidamente si los punteros alias y ejecute el código más rápido si determina que está bien.

Dicho esto, mantengo mi comentario sobre simplemente medir el rendimiento de las dos versiones, mi dinero está en no ver ninguna diferencia de rendimiento constante entre las dos versiones del código.

En mi opinión, preguntas como estas están bien si su propósito es aprender sobre las teorías y técnicas de optimización del compilador, pero es una pérdida de tiempo (una micro-optimización inútil) si su objetivo final aquí es hacer que el programa se ejecute más rápido.

SirGuy
fuente
1
@GuyGreer: Es un importante bloqueador de optimización; Considero desafortunado que las reglas del lenguaje se centren en reglas sobre tipos efectivos, en lugar de identificar situaciones en las que las escrituras y lecturas de diferentes elementos estén o no sin secuencia. Las reglas escritas en tal término podrían hacer un trabajo mucho mejor para satisfacer las necesidades del compilador y del programador que las actuales.
supercat
3
@GuyGreer: ¿no sería un restrictcalificador la respuesta al problema de alias en este caso?
LThode
4
En mi experiencia, restrictes en gran parte impredecible. MSVC es el único compilador que he visto que parece hacerlo correctamente. ICC pierde información de alias a través de llamadas a funciones incluso si están en línea. Y GCC generalmente no obtiene ningún beneficio a menos que declare cada parámetro de entrada como restrict(incluidas las thisfunciones miembro).
Mysticial
1
@Mystical: Una cosa para recordar es que chartodos los tipos de alias, así que si tienes un char *, entonces tienes que usarlo restricten todo. O si ha eliminado las estrictas reglas de alias de GCC, -fno-strict-aliasingentonces todo se considera un posible alias.
Zan Lynx
1
@Ray La propuesta más reciente para la restrictsemántica -like en C ++ es N4150 .
TC
24

Bien, chicos, he medido con GCC -O3(usando GCC 4.9 en Linux x64).

¡Resulta que la segunda versión funciona un 54% más rápido!

Entonces, supongo que el uso de alias es lo importante, no lo había pensado.

[Editar]

Probé de nuevo la primera versión con todos los punteros definidos con __restrict__y los resultados son los mismos. Extraño ... O el alias no es el problema o, por alguna razón, el compilador no lo optimiza bien incluso con __restrict__.

[Editar 2]

Ok, creo que pude demostrar que el problema es el alias. Repetí mi prueba original, esta vez usando una matriz en lugar de un puntero:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Y medido (tenía que usar "-mcmodel = large" para vincularlo). Entonces probé:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Los resultados de la medida fueron los mismos: parece que el compilador pudo optimizarlo por sí mismo.

Luego probé los códigos originales (con un puntero p), esta vez cuando pes de tipo std::uint16_t*. Nuevamente, los resultados fueron los mismos, debido al estricto aliasing. Luego intenté construir con "-fno-estricto-aliasing", y nuevamente vi una diferencia en el tiempo.

Yaron Cohen-Tal
fuente
4
Parece que debería ser un comentario, aunque técnicamente responde a la pregunta. También tenga en cuenta que, lamentablemente, no ha demostrado que el uso de alias sea lo mejor. Parece probable, ciertamente plausible, pero eso es diferente a concluir que eso fue todo.
SirGuy
@GuyGreer: Mira mi [edición 2] - ahora creo que está bastante probado.
Yaron Cohen-Tal
2
Me pregunto por qué empezaste a usar la variable "i" cuando tienes "x" en tu ciclo.
Jesper Madsen
1
¿Soy yo quien encuentra la frase un 54% más rápida de comprender? ¿Quiere decir que es 1,54 veces la velocidad del no optimizado, o algo más?
Roddy
3
@ YaronCohen-Tal, ¿más del doble de rápido? Impresionante, ¡pero no es lo que hubiera entendido que significaba "54% más rápido"!
Roddy
24

Otras respuestas han señalado que sacar la operación del puntero del bucle puede cambiar el comportamiento definido debido a las reglas de alias que permiten que char alias cualquier cosa y, por lo tanto, no es una optimización permitida para un compilador, aunque en la mayoría de los casos es obviamente correcto para un humano. programador.

También han señalado que sacar la operación del bucle suele ser, aunque no siempre, una mejora desde el punto de vista del rendimiento y, a menudo, es negativo desde el punto de vista de la legibilidad.

Me gustaría señalar que a menudo existe una "tercera vía". En lugar de contar hasta el número de iteraciones que desea, puede realizar una cuenta regresiva hasta cero. Esto significa que el número de iteraciones solo se necesita una vez al comienzo del ciclo, no tiene que almacenarse después de eso. Mejor aún, a nivel de ensamblador, a menudo elimina la necesidad de una comparación explícita, ya que la operación de decremento generalmente establecerá indicadores que indican si el contador era cero antes (indicador de acarreo) y después (indicador de cero) del decremento.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Tenga en cuenta que esta versión del bucle da valores de x en el rango 1..ancho en lugar del rango 0 .. (ancho-1). Eso no importa en su caso porque en realidad no está usando x para nada, pero es algo que debe tener en cuenta. Si desea un ciclo de cuenta atrás con valores de x en el rango 0 .. (ancho-1), puede hacerlo.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

También puede deshacerse de las conversiones en los ejemplos anteriores si lo desea sin preocuparse por su impacto en las reglas de comparación, ya que todo lo que está haciendo con mapa de bits-> ancho es asignarlo directamente a una variable.

enchufar
fuente
2
He visto ese segundo caso formateado como x --> 0, lo que resulta en el operador "downto". Muy gracioso. PD: No considero que hacer una variable para la condición final sea negativa para la legibilidad, en realidad puede ser lo contrario.
Mark Ransom
Realmente depende, a veces una declaración se vuelve tan horrible que dividirla en varias declaraciones mejora la legibilidad, pero no creo que ese sea el caso aquí.
Plugwash
1
+1 Buena observación, aunque yo diría que levantar static_cast<unsigned>(bitmap->width)y usar widthen su lugar en el bucle es en realidad una mejora para la legibilidad porque ahora hay menos cosas para que el lector analice por línea. Sin embargo, las opiniones de otros pueden diferir.
SirGuy
1
Hay muchas otras situaciones en las que contar hacia abajo es mejor (por ejemplo, al eliminar elementos de una lista). No sé por qué esto no se hace con más frecuencia.
Ian Goldby
3
Si desea escribir bucles que se parezcan más al conjunto óptimo, utilice do { } while(), porque en ASM hace bucles con una rama condicional al final. Los bucles for(){}y habituales while(){}requieren instrucciones adicionales para probar la condición del bucle una vez antes del bucle, si el compilador no puede probar que siempre se ejecuta al menos una vez. Por supuesto, use for()o while()cuando sea útil para verificar si el ciclo debería ejecutarse una vez, o cuándo es más legible.
Peter Cordes
11

Lo único aquí que puede evitar la optimización es la estricta regla de alias . Resumiendo :

"El alias estricto es una suposición, hecha por el compilador de C (o C ++), que los punteros de desreferenciación a objetos de diferentes tipos nunca se referirán a la misma ubicación de memoria (es decir, alias entre sí)".

[…]

La excepción a la regla es a char*, que puede apuntar a cualquier tipo.

La excepción también se aplica a unsignedy signed charpunteros.

Este es el caso en su código: está modificando a *ptravés de pcuál es un unsigned char*, por lo que el compilador debe asumir que podría apuntar a bitmap->width. Por lo tanto, el almacenamiento en caché de bitmap->widthes una optimización no válida. Este comportamiento de prevención de optimización se muestra en la respuesta de YSC .

Si y solo si se papunta a un tipo que no es chary que no es de decltype(bitmap->width)tipo, el almacenamiento en caché sería una posible optimización.

emlai
fuente
10

La pregunta originalmente formulada:

¿Vale la pena optimizarlo?

Y mi respuesta a eso (obteniendo una buena combinación de votos a favor y en contra ...)

Deje que el compilador se preocupe por eso.

Es casi seguro que el compilador hará un mejor trabajo que usted. Y no hay garantía de que su 'optimización' sea mejor que el código 'obvio'. ¿Lo ha medido?

Más importante aún, ¿tiene alguna prueba de que el código que está optimizando tenga algún impacto en el rendimiento de su programa?

A pesar de los votos negativos (y ahora veo el problema del alias), todavía estoy contento con eso como una respuesta válida. Si no sabe si vale la pena optimizar algo, probablemente no lo sea.

Una pregunta bastante diferente, por supuesto, sería la siguiente:

¿Cómo puedo saber si vale la pena optimizar un fragmento de código?

Primero, ¿su aplicación o biblioteca debe ejecutarse más rápido de lo que lo hace actualmente? ¿El usuario ha esperado demasiado? ¿Su software pronostica el tiempo de ayer en lugar del de mañana?

Solo usted puede saberlo, basándose en para qué sirve su software y en lo que esperan sus usuarios.

Suponiendo que su software necesita alguna optimización, lo siguiente que debe hacer es comenzar a medir. Los perfiladores le dirán dónde pasa su código, es hora. Si su fragmento no se muestra como un cuello de botella, es mejor dejarlo solo. Los perfiladores y otras herramientas de medición también le dirán si sus cambios han marcado una diferencia. Es posible pasar horas intentando optimizar el código, solo para descubrir que no ha hecho una diferencia apreciable.

De todos modos, ¿a qué te refieres con "optimizar"?

Si no está escribiendo un código 'optimizado', entonces su código debe ser tan claro, limpio y conciso como pueda. El argumento "La optimización prematura es mala" no es una excusa para un código descuidado o ineficiente.

El código optimizado normalmente sacrifica algunos de los atributos anteriores por el rendimiento. Podría implicar la introducción de variables locales adicionales, tener objetos con un alcance más amplio de lo esperado o incluso invertir el orden de bucle normal. Todos estos pueden ser menos claros o concisos, así que documente el código (¡brevemente!) Sobre por qué está haciendo esto.

Pero a menudo, con código "lento", estas microoptimizaciones son el último recurso. El primer lugar para mirar son los algoritmos y las estructuras de datos. ¿Hay alguna forma de evitar hacer el trabajo? ¿Se pueden reemplazar las búsquedas lineales por búsquedas binarias? ¿Una lista enlazada sería más rápida aquí que un vector? ¿O una mesa hash? ¿Puedo almacenar en caché los resultados? ¡Tomar buenas decisiones 'eficientes' aquí a menudo puede afectar el desempeño en un orden de magnitud o más!

Roddy
fuente
12
Cuando está iterando sobre el ancho de una imagen de mapa de bits, la lógica de bucle puede ser una parte significativa del tiempo invertido en el bucle. En lugar de preocuparse por la optimización prematura, en este caso es mejor desarrollar las mejores prácticas que sean eficientes desde el principio.
Mark Ransom
4
@MarkRansom estuvo de acuerdo, en parte: Pero las "mejores prácticas" serían: usar una biblioteca existente o una llamada a la API para llenar imágenes, ob: hacer que la GPU lo haga por usted. Nunca debería ser el tipo de microoptimización no medida que sugiere el OP. ¿Y cómo sabe que este código se ejecuta más de una vez, o con mapas de bits de más de 16 píxeles de ancho ...?
Roddy
@Veedrac. Aprecie la justificación del -1. La idea central de la pregunta ha cambiado sutil y sustancialmente desde que respondí. Si crees que la respuesta (ampliada) sigue siendo inútil, es hora de que la elimine ... "Vale la pena ..." siempre se basa principalmente en opiniones, de todos modos.
Roddy
@Roddy Aprecio las ediciones, ayudan (y mi comentario probablemente sonó demasiado duro de todos modos). Sin embargo, todavía estoy indeciso, ya que esta es realmente una respuesta a una pregunta que no es adecuada para Stack Overflow. Parece que una respuesta adecuada sería específica del fragmento, como lo son las respuestas altamente votadas aquí.
Veedrac
6

Utilizo el siguiente patrón en una situación como esta. Es casi tan corto como el primer caso suyo, y es mejor que el segundo caso, porque mantiene la variable temporal local en el ciclo.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Esto será más rápido con un compilador menos inteligente, una compilación de depuración o ciertos indicadores de compilación.

Edit1 : colocar una operación constante fuera de un bucle es un buen patrón de programación. Muestra comprensión de los conceptos básicos del funcionamiento de la máquina, especialmente en C / C ++. Yo diría que el esfuerzo por demostrar su valía debería estar en personas que no sigan esta práctica. Si el compilador castiga por un buen patrón, es un error en el compilador.

Edit2: He medido mi sugerencia con el código original en vs2013, obtuve% 1 de mejora. ¿Podemos hacerlo mejor? Una simple optimización manual ofrece una mejora 3 veces mayor que el bucle original en la máquina x64 sin recurrir a instrucciones exóticas. El código siguiente asume un sistema little endian y un mapa de bits correctamente alineado. TEST 0 es original (9 seg), TEST 1 es más rápido (3 seg). Apuesto a que alguien podría hacer esto aún más rápido, y el resultado de la prueba dependería del tamaño del mapa de bits. Definitivamente, pronto en el futuro, el compilador podrá producir código consistentemente más rápido. Me temo que este será el futuro cuando el compilador también sea un programador de IA, por lo que estaríamos sin trabajo. Pero por ahora, simplemente escriba un código que muestre que sabe que no se necesita una operación adicional en el ciclo.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}
0kgatos
fuente
Puede ahorrar otro 25% en 64 bits si usa tres int64_t en lugar de int64_t e int32_t.
Antonín Lejsek
5

Hay dos cosas a considerar.

A) ¿Con qué frecuencia se ejecutará la optimización?

Si la respuesta no es muy frecuente, como solo cuando un usuario hace clic en un botón, no se moleste si hace que su código sea ilegible. Si la respuesta es 1000 veces por segundo, probablemente querrá ir con la optimización. Si es incluso un poco complejo, asegúrese de incluir un comentario para explicar lo que está sucediendo para ayudar al próximo chico que se presente.

B) ¿Esto hará que el código sea más difícil de mantener / solucionar?

Si no está viendo una gran ganancia en el rendimiento, hacer que su código sea críptico simplemente para guardar unos pocos tics de reloj no es una buena idea. Mucha gente le dirá que cualquier buen programador debería poder ver el código y averiguar qué está pasando. Esto es verdad. El problema es que en el mundo empresarial el tiempo extra para averiguarlo cuesta dinero. Entonces, si puedes hacerlo más bonito de leer, hazlo. Tus amigos te lo agradecerán.

Dicho esto, personalmente usaría el ejemplo B.

soulsabr
fuente
4

El compilador puede optimizar muchas cosas. Para su ejemplo, debe optar por la legibilidad, la mantenibilidad y lo que sigue el estándar de su código. Para obtener más información sobre lo que se puede optimizar (con GCC), consulte esta publicación de blog .

Guillaume Racicot
fuente
4

Como regla general, deje que el compilador haga la optimización por usted, hasta que determine que debe hacerse cargo. La lógica de esto no tiene nada que ver con el rendimiento, sino con la legibilidad humana. En la gran mayoría de los casos, la legibilidad de su programa es más importante que su rendimiento. Debe apuntar a escribir código que sea más fácil de leer para un humano, y luego solo preocuparse por la optimización cuando esté convencido de que el rendimiento es más importante que la capacidad de mantenimiento de su código.

Una vez que vea que el rendimiento es importante, debe ejecutar un generador de perfiles en el código para determinar qué bucles son ineficientes y optimizarlos individualmente. De hecho, puede haber casos en los que desee hacer esa optimización (especialmente si migra hacia C ++, donde se involucran los contenedores STL), pero el costo en términos de legibilidad es grande.

Además, puedo pensar en situaciones patológicas en las que podría ralentizar el código. Por ejemplo, considere el caso en el que el compilador no pudo probar que bitmap->widthera constante durante el proceso. Al agregar la widthvariable, fuerza al compilador a mantener una variable local en ese ámbito. Si, por alguna razón específica de la plataforma, esa variable adicional impidió alguna optimización del espacio de pila, es posible que deba reorganizar la forma en que emite códigos de bytes y producir algo menos eficiente.

Como ejemplo, en Windows x64, uno está obligado a llamar a una llamada API especial, __chkstken el preámbulo de la función si la función usará más de 1 página de variables locales. Esta función le da a Windows la oportunidad de administrar las páginas de protección que utilizan para expandir la pila cuando sea necesario. Si su variable adicional aumenta el uso de la pila de menos de 1 página a 1 página o más, su función ahora está obligada a llamar __chkstkcada vez que se ingresa. Si tuviera que optimizar este bucle en una ruta lenta, ¡en realidad podría ralentizar la ruta rápida más de lo que ahorró en la ruta lenta!

Claro, es un poco patológico, pero el punto de ese ejemplo es que puedes ralentizar el compilador. Simplemente muestra que tiene que perfilar su trabajo para determinar dónde van las optimizaciones. Mientras tanto, no sacrifique la legibilidad de ninguna manera por una optimización que puede o no importar.

Cort Ammon
fuente
4
Me gustaría que C y C ++ proporcionaran más formas de identificar explícitamente las cosas que no le importan al programador. No solo brindarían más oportunidades para que los compiladores optimicen las cosas, sino que también evitarían que otros programadores que lean el código tengan que adivinar si, por ejemplo, podría estar volviendo a verificar el mapa de bits-> ancho cada vez para asegurarse de que los cambios afecten al bucle, o si podría estar almacenando en caché mapa de bits-> ancho para asegurarse de que los cambios no afecten al bucle. Tener un medio de decir "Caché esto o no - no me importa" dejaría en claro el motivo de la elección del programador.
supercat
@supercat Estoy totalmente de acuerdo, ya que uno puede ver si uno mira las pilas de lenguajes fallidos tatuados que he tratado de escribir para resolver esto. He descubierto que es muy difícil definir "lo que" a alguien no le importa sin tanta sintaxis impía que simplemente no vale la pena. Continúo mi búsqueda en vano.
Cort Ammon
No es posible definirlo en todos los casos, pero creo que hay muchos casos en los que el sistema de tipos podría ayudar. C también decidió hacer que los tipos de caracteres sean el "acceso universal" en lugar de tener un calificador de tipo que sea un poco más flexible que "volátil" que podría aplicarse a cualquier tipo, con la semántica de que los accesos de tales tipos se procesarían en secuencia con accesos del tipo equivalente no calificado y también con accesos de todo tipo de variables con ese mismo calificador. Eso ayudaría a aclarar si uno estaba usando tipos de caracteres porque necesitaba el ...
supercat
... comportamiento de aliasing, o si uno los estaba usando porque eran del tamaño adecuado para satisfacer sus necesidades. También sería útil tener barreras de alias explícitas que en muchos casos podrían colocarse fuera de los bucles, a diferencia de las barreras implícitas asociadas con los accesos de tipo carácter.
supercat
1
Esta es una charla inteligente, pero, en general, si ya selecciona C para su tarea, probablemente el rendimiento sea muy importante y se deberían aplicar las diferentes reglas. De lo contrario, puede ser mejor usar Ruby, Java, Python o algo parecido.
Audrius Meskauskas
4

La comparación es incorrecta ya que los dos fragmentos de código

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

y

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

no son equivalentes

En el primer caso, widthes dependiente y no constante, y no se puede suponer que no cambie entre iteraciones posteriores. Por lo tanto, no se puede optimizar, pero debe comprobarse en cada bucle .

En su caso optimizado, a una variable local se le asigna el valor de bitmap->widthen algún momento durante la ejecución del programa. El compilador puede verificar que esto no cambie realmente.

¿Pensó en subprocesos múltiples, o tal vez el valor podría depender externamente de manera que su valor sea volátil? ¿Cómo se puede esperar que el compilador resuelva todas estas cosas si usted no lo dice?

El compilador solo puede funcionar tan bien como su código lo permita.

g24l
fuente
2

A menos que sepa exactamente cómo el compilador optimiza el código, es mejor hacer sus propias optimizaciones manteniendo la legibilidad y el diseño del código. Prácticamente es difícil verificar el código ensamblador para cada función que escribimos para las nuevas versiones del compilador.

Vinayak SM
fuente
1

El compilador no puede optimizar bitmap->widthporque el valor de widthse puede cambiar entre iteraciones. Hay algunas razones más comunes:

  1. Multi-hilo. El compilador no puede predecir si otro hilo está a punto de cambiar de valor.
  2. Modificación dentro del ciclo, a veces no es fácil saber si la variable se cambiará dentro del ciclo.
  3. Es llamada a la función, por ejemplo, iterator::end()o container::size()por lo que es difícil predecir si siempre se devuelve el mismo resultado.

Para resumir (mi opinión personal) para los lugares que requieren un alto nivel de optimización, debe hacerlo usted mismo, en otros lugares simplemente déjelo, el compilador puede optimizarlo o no, si no hay una gran diferencia, la legibilidad del código es el objetivo principal.

ST3
fuente