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

*pes del mismo tipo quewidthentonces, no es trivial optimizarlo, ya queppodría apuntarwidthy modificarlo dentro del ciclo.papunte 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.cppnota: este código no debe ejecutarse.
Fuente
optimized.cppnota: este código no debe ejecutarse.
Compilacion
$ g++ -s -O3 unoptimized.cpp$ g++ -s -O3 optimized.cppMontaje (no optimizado)
Montaje (optimizado.s)
diff
El ensamblado generado para la versión optimizada realmente carga (
lea) lawidthconstante a diferencia de la versión no optimizada que calcula elwidthdesplazamiento en cada iteración (movq).Cuando tenga tiempo, eventualmente publico algún punto de referencia sobre eso. Buena pregunta.
fuente
const unsignedlugar de solounsigneden el caso no optimizado.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.bitmapes 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
pybitmapque apuntan a la misma ubicación en la memoria, pero el compilador no sabe que y (porquepes de tipochar*) el compilador tiene que hacer este trabajo código incluso sipybitmapsuperposición.Esto significa en este caso que si el bucle cambia a
bitmap->widthtravés del puntero,pentonces debe verse cuando se vuelva a leerbitmap->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.
fuente
restrictcalificador la respuesta al problema de alias en este caso?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 comorestrict(incluidas lasthisfunciones miembro).chartodos los tipos de alias, así que si tienes un char *, entonces tienes que usarlorestricten todo. O si ha eliminado las estrictas reglas de alias de GCC,-fno-strict-aliasingentonces todo se considera un posible alias.restrictsemá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 cuandopes 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 usarwidthen 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
unsignedysignedcharpunteros.Este es el caso en su código: está modificando a
*ptravés depcuá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->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 eschary 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->widthera constante durante el proceso. Al agregar lawidthvariable, 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.
fuente
La comparación es incorrecta ya que los dos fragmentos de código
y
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.
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->widthporque el valor dewidthse 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