Considere el siguiente código ( p
es de tipo unsigned char*
y bitmap->width
es 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.
fuente
*p
es del mismo tipo quewidth
entonces, no es trivial optimizarlo, ya quep
podría apuntarwidth
y modificarlo dentro del ciclo.p
apunte a la misma memoria quebitmap->width
. Por lo tanto, no puedo optimizar legalmente el primer ejemplo al segundo.Respuestas:
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.
Fuente
optimized.cpp
nota: este código no debe ejecutarse.
Compilacion
$ g++ -s -O3 unoptimized.cpp
$ g++ -s -O3 optimized.cpp
Montaje (no optimizado)
Montaje (optimizado.s)
diff
El ensamblado generado para la versión optimizada realmente carga (
lea
) lawidth
constante a diferencia de la versión no optimizada que calcula elwidth
desplazamiento en cada iteración (movq
).Cuando tenga tiempo, eventualmente publico algún punto de referencia sobre eso. Buena pregunta.
fuente
const unsigned
lugar de solounsigned
en el caso no optimizado.main
de 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.bitmap
es global. La versión que no es CSEd usa un operando de memoria paracmp
, 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.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
p
ybitmap
que apuntan a la misma ubicación en la memoria, pero el compilador no sabe que y (porquep
es de tipochar*
) el compilador tiene que hacer este trabajo código incluso sip
ybitmap
superposición.Esto significa en este caso que si el bucle cambia a
bitmap->width
través del puntero,p
entonces debe verse cuando se vuelva a leerbitmap->width
má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.
fuente
restrict
calificador la respuesta al problema de alias en este caso?restrict
es 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 comorestrict
(incluidas lasthis
funciones miembro).char
todos los tipos de alias, así que si tienes un char *, entonces tienes que usarlorestrict
en todo. O si ha eliminado las estrictas reglas de alias de GCC,-fno-strict-aliasing
entonces todo se considera un posible alias.restrict
semántica -like en C ++ es N4150 .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:
Y medido (tenía que usar "-mcmodel = large" para vincularlo). Entonces probé:
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 cuandop
es de tipostd::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.fuente
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.
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.
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.
fuente
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.static_cast<unsigned>(bitmap->width)
y usarwidth
en 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.do { } while()
, porque en ASM hace bucles con una rama condicional al final. Los buclesfor(){}
y habitualeswhile(){}
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, usefor()
owhile()
cuando sea útil para verificar si el ciclo debería ejecutarse una vez, o cuándo es más legible.Lo único aquí que puede evitar la optimización es la estricta regla de alias . Resumiendo :
La excepción también se aplica a
unsigned
ysigned
char
punteros.Este es el caso en su código: está modificando a
*p
través dep
cuál es ununsigned char*
, por lo que el compilador debe asumir que podría apuntar abitmap->width
. Por lo tanto, el almacenamiento en caché debitmap->width
es 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
p
apunta a un tipo que no eschar
y que no es dedecltype(bitmap->width)
tipo, el almacenamiento en caché sería una posible optimización.fuente
La pregunta originalmente formulada:
Y mi respuesta a eso (obteniendo una buena combinación de votos a favor y en contra ...)
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:
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.
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!
fuente
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.
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.
fuente
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.
fuente
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 .
fuente
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->width
era constante durante el proceso. Al agregar lawidth
variable, 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,
__chkstk
en 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__chkstk
cada 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.
fuente
La comparación es incorrecta ya que los dos fragmentos de código
y
no son equivalentes
En el primer caso,
width
es 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->width
en 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.
fuente
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.
fuente
El compilador no puede optimizar
bitmap->width
porque el valor dewidth
se puede cambiar entre iteraciones. Hay algunas razones más comunes:iterator::end()
ocontainer::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.
fuente