En general, ¿con qué frecuencia y cuándo debo optimizar mi código?

13

En la programación de negocios 'normal', el paso de optimización a menudo se deja hasta que realmente se necesita. Lo que significa que no debe optimizar hasta que sea realmente necesario.

Recuerde lo que dijo Donald Knuth : "Debemos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal"

¿Cuándo es el momento de optimizar para asegurarse de que no estoy desperdiciando esfuerzo? ¿Debo hacerlo a nivel de método? Nivel de clase? Nivel de módulo?

Además, ¿cuál debería ser mi medida de optimización? Garrapatas? ¿Cuadros por segundo? ¿Tiempo Total?

David Basarab
fuente

Respuestas:

18

Donde he trabajado, siempre usamos múltiples niveles de creación de perfiles; Si ve un problema, simplemente baje un poco más en la lista hasta que descubra lo que está sucediendo:

  • El "perfilador humano", también conocido como solo jugar el juego ; ¿Se siente lento o "enganche" ocasionalmente? ¿Te das cuenta de animaciones desiguales? (Como desarrollador, tenga en cuenta que será más sensible a algunos tipos de problemas de rendimiento y ajeno a otros. Planifique las pruebas adicionales en consecuencia).
  • Encienda la pantalla FPS , que es un FPS promedio de 5 segundos con ventana deslizante. Muy poca sobrecarga para calcular y mostrar.
  • Active las barras de perfil , que son solo una serie de quads (colores ROYGBIV) que representan diferentes partes del marco (por ejemplo, vblank, preframe, update, collision, render, postframe) usando un temporizador simple "cronómetro" alrededor de cada sección de código . Para enfatizar lo que queremos, configuramos un ancho de pantalla equivalente a un marco objetivo de 60Hz, por lo que es realmente fácil ver si está, por ejemplo, un 50% por debajo del presupuesto (solo media barra) o un 50% por encima ( la barra se enrolla y se convierte en una barra y media). También es bastante fácil saber qué es lo que generalmente consume la mayor parte del marco: rojo = renderizado, amarillo = actualización, etc.
  • Cree una compilación instrumentada especial que inserte "cronómetro" como código alrededor de todas y cada una de las funciones. (Tenga en cuenta que puede obtener un rendimiento masivo, dcache e icache al hacer esto, por lo que definitivamente es intrusivo. Pero si carece de un perfil de muestreo adecuado o un soporte decente en la CPU, esta es una opción aceptable. También puede ser inteligente sobre el registro de un mínimo de datos sobre la función enter / exit y la reconstrucción de calltraces más tarde). Cuando creamos el nuestro, imitamos gran parte del formato de salida de gprof .
  • Lo mejor de todo, ejecute un generador de perfiles de muestreo ; VTune y CodeAnalyst están disponibles para x86 y x64, tiene varios entornos de simulación o emulación que pueden proporcionarle datos aquí.

(Hay una historia divertida del GDC del año pasado de un programador de gráficos que tomó cuatro fotos de sí mismo: feliz, indiferente, molesto y enojado, y mostró una imagen apropiada en la esquina de las construcciones internas en función de la velocidad de fotogramas. los creadores de contenido aprendieron rápidamente a no activar sombreadores complicados para todos sus objetos y entornos: harían enojar al programador. Contemplen el poder de la retroalimentación).

Tenga en cuenta que también puede hacer cosas divertidas como graficar las "barras de perfil" continuamente, para que pueda ver patrones de espiga ("estamos perdiendo un cuadro cada 7 cuadros") o similares.

Para responder a su pregunta directamente, sin embargo: en mi experiencia, mientras que es tentador (y, a menudo recompensar - Por lo general aprendo algo) para volver a escribir funciones / módulos individuales al número Optimizar de instrucciones o Icaché o dcache rendimiento, y lo hacemos realmente necesita hacer esto a veces cuando tenemos un problema de rendimiento particularmente desagradable, la gran mayoría de los problemas de rendimiento que tratamos regularmente se reducen al diseño . Por ejemplo:

  • ¿Deberíamos almacenar en caché en RAM o recargar desde el disco los cuadros de animación de estado de "ataque" para el jugador? ¿Qué tal para cada enemigo? No tenemos RAM para hacerlos todos, ¡pero las cargas de disco son caras! ¡Puedes ver el enganche si aparecen 5 o 6 enemigos diferentes a la vez! (Bien, ¿qué tal escalonar el desove?)
  • ¿Estamos haciendo un solo tipo de operación en todas las partículas, o todas las operaciones en una sola partícula? (Esta es una compensación icache / dcache, y la respuesta no siempre es clara). ¿Qué tal si separamos todas las partículas y almacenamos las posiciones juntas (la famosa "estructura de matrices") versus mantener todos los datos de partículas en un solo lugar (" matriz de estructuras ").

Lo escuchas hasta que se vuelve desagradable en cualquier curso de informática de nivel universitario, pero: realmente se trata de estructuras de datos y algoritmos. Dedicar algo de tiempo al algoritmo y al diseño del flujo de datos le dará más beneficios en general. (Asegúrese de haber leído las excelentes diapositivas de las diapositivas de la programación orientada a objetos de un compañero de Sony Developer Services para obtener una idea aquí). Esto no "se siente" como una optimización; es principalmente el tiempo que se pasa con una pizarra o herramienta UML o creando muchos prototipos, en lugar de hacer que el código actual se ejecute más rápido. Pero generalmente vale mucho más la pena.

Y otra heurística útil: si está cerca del "núcleo" de su motor, puede valer la pena un esfuerzo extra y experimentación para optimizar (por ejemplo, ¡vectorice esas multiplicaciones matriciales!). Cuanto más lejos del núcleo, menos debería preocuparse por eso a menos que una de sus herramientas de creación de perfiles le indique lo contrario.

leander
fuente
6
  1. Utilice las estructuras de datos y algoritmos correctos por adelantado.
  2. No micro-optimice hasta su perfil y sepa exactamente dónde están sus puntos calientes.
  3. No te preocupes por ser inteligente. El compilador ya hace todos los pequeños trucos en los que estás pensando ("¡Oh! ¡Necesito multiplicar por cuatro! ¡Cambiaré a la izquierda dos!")
  4. Presta atención a los errores de caché.
munificente
fuente
1
Confiar en el compilador solo es inteligente hasta cierto punto. Sí, hará algunas optimizaciones de mirilla en las que no pensaría (y no podría hacerlo sin ensamblar), pero no tiene idea de lo que se supone que debe hacer su algoritmo, por lo que no puede hacer optimizaciones inteligentes. Además, se sorprenderá de cuántos ciclos puede ganar implementando código crítico en ensamblaje o intrínseco ... si sabe lo que está haciendo. Los compiladores no son tan inteligentes como se supone que son, no saben lo que haces a menos que les digas explícitamente en todas partes (como usar 'restringir' religiosamente).
Kaj
1
Y nuevamente debo comentar que si solo busca puntos calientes, se perderá muchos ciclos porque no encontrará ningún ciclo filtrado (por ejemplo, punteros inteligentes ... desreferencias en cualquier lugar, nunca apareciendo como punto de acceso porque efectivamente todo su programa es un punto de acceso).
Kaj
1
Estoy de acuerdo con sus dos puntos, pero agregaría la mayor parte de eso en "usar las estructuras de datos y algoritmos correctos". Si está pasando punteros inteligentes con recuento de referencias en todas partes y está realizando ciclos de sangría a través del conteo, definitivamente ha elegido la estructura de datos incorrecta.
munificent
5

Sin embargo, recuerde también "pesimismo prematuro". Si bien no es necesario volverse duro en cada línea de código, hay justificación para darse cuenta de que realmente está trabajando en un juego, lo que tiene implicaciones de rendimiento en tiempo real.
Si bien todos le dicen que mida y optimice los puntos calientes, esa técnica no le mostrará el rendimiento que se pierde en lugares ocultos. Por ejemplo, si cada operación '+' en su código demoraría el doble de lo que debería, no aparecerá como un punto caliente y, por lo tanto, nunca lo optimizará ni siquiera se dará cuenta, ya que se está utilizando para colocarlo puede costarle mucho rendimiento. Te sorprendería cuántos de esos ciclos se filtran sin ser detectados. Así que sé consciente de lo que haces.
Aparte de eso, tiendo a perfilar regularmente para tener una idea de lo que hay allí y cuánto tiempo queda por fotograma. Para mí, el tiempo por fotograma es el más lógico, ya que me dice directamente dónde estoy con los objetivos de velocidad de fotogramas. También trate de averiguar dónde están los picos y qué los causa: prefiero una velocidad de fotogramas estable sobre una velocidad de fotogramas alta con picos.

Kaj
fuente
Esto me parece muy mal. Claro, mi '+' puede tomar el doble de tiempo cada vez que se llama, pero esto realmente solo importa en un ciclo cerrado. Dentro de un ciclo cerrado, cambiar un solo '+' puede hacer órdenes de magnitud más que cambiar un '+' fuera del ciclo. ¿Por qué pensar en una décima de microsegundo, cuando se puede guardar un milisegundo?
Wilduck
1
Entonces no entiendes la idea detrás de la pérdida por goteo. '+' (solo como ejemplo) se llama cientos de miles de veces por cuadro, no solo en bucles cerrados. Si eso pierde algunos ciclos cada vez que pierde muchos ciclos en todos los ámbitos, pero nunca se mostrará como un punto de acceso ya que las llamadas se distribuyen de manera uniforme en su base de código / ruta de ejecución. Entonces, no estás hablando de una décima de microsegundo, sino de hecho miles de esas décimas de microsegundos, lo que suma varios milisegundos. Después de ir por la fruta baja (bucles apretados) gané milisegundos de esta manera más de una vez.
Kaj
Es como un grifo que gotea. ¿Por qué preocuparse por salvar esa pequeña gota? - "Si su grifo gotea a razón de una gota por segundo, puede esperar desperdiciar 2700 galones por año".
Kaj
Oh, supongo que no estaba claro a qué me refería cuando el operador + estaba sobrecargado, por lo que afectaría a cada '+' en el código; de hecho, no querría optimizar cada '+' en el código. Mal ejemplo, supongo ... Lo dije como un ejemplo de 'funcionalidad central que se llama por todas partes donde la implementación podría ser más lenta de lo que se suponía, especialmente cuando está oculta por sobrecarga del operador u otras construcciones ofuscadoras de C ++'.
Kaj
3

Una vez que un juego está listo para ser lanzado (ya sea final o beta), o es notablemente lento, ese es probablemente el mejor momento para perfilar su aplicación. Por supuesto, siempre puede ejecutar el generador de perfiles en cualquier momento; pero sí, la optimización prematura es la raíz de todo mal. Optimización infundada, también; necesita datos reales para mostrar que algunos códigos son lentos, antes de intentar "optimizarlos". Un perfilador lo hace por ti.

Si no sabes sobre un perfilador, ¡aprende! Aquí hay una buena publicación de blog que demuestra la utilidad de un generador de perfiles.

La mayor parte de la optimización del código del juego se reduce a reducir los ciclos de CPU que necesita para cada cuadro. Una forma de hacerlo es optimizar cada rutina a medida que la escribe y asegurarse de que sea lo más rápido posible. Sin embargo, hay un dicho común de que el 90% de los ciclos de la CPU se gastan en el 10% del código. Esto significa que dirigir todo su trabajo de optimización a estas rutinas de cuello de botella tendrá 10 veces el efecto de optimizar todo de manera uniforme. Entonces, ¿cómo identificas estas rutinas? La creación de perfiles lo hace fácil.

De lo contrario, si su pequeño juego se ejecuta a 200 FPS a pesar de que tiene un algoritmo ineficiente, ¿realmente tiene una razón para optimizar? Debes tener una buena idea de las especificaciones de tu máquina objetivo y asegurarte de que el juego se ejecute bien en esa máquina, pero cualquier cosa más allá de eso es (posiblemente) tiempo perdido que podría gastarse mejor codificando o puliendo el juego.

Ricket
fuente
Si bien la fruta baja tiende a estar en el 10% del código, y se atrapa fácilmente al crear un perfil al final, simplemente trabajando al crear un perfil para esto te hará perder las rutinas que se llaman mucho pero tienen solo un poco un poco de código incorrecto cada uno: no aparecerán en su perfil, pero sangran muchos ciclos por llamada. Realmente suma.
Kaj
@Kaj, los buenos perfiladores suman todos los cientos de ejecuciones individuales del algoritmo malo y le muestran el total. Luego dirás "¿Pero qué pasaría si tuvieras 10 métodos incorrectos y todos llamaran a 1/10 de la frecuencia?" Si pasas todo tu tiempo en esos 10 métodos, te perderás toda la fruta baja donde obtendrás una inversión mucho mayor por tu dinero.
John McDonald
2

Me resulta útil construir perfiles. Incluso si no está optimizando activamente, es bueno tener una idea de lo que está limitando su rendimiento en un momento dado. Muchos juegos tienen algún tipo de HUD superpuesto que muestra una tabla gráfica simple (generalmente solo una barra de color) que muestra cuánto tiempo duran varias partes del bucle del juego en cada fotograma.

Sería una mala idea dejar el análisis y la optimización del rendimiento en una etapa demasiado tardía. Si ya has creado el juego y estás un 200% por encima del presupuesto de tu CPU y no puedes encontrarlo mediante la optimización, estás jodido.

Necesitas saber cuáles son los presupuestos para gráficos, física, etc., mientras escribes. No puede hacer eso si no tiene idea de cuál será su rendimiento, y no puede adivinar eso sin saber cuál es su rendimiento y qué tanta holgura podría haber.

Así que incorpore algunas estadísticas de rendimiento desde el primer día.

En cuanto a cuándo abordar cosas, nuevamente, probablemente sea mejor no dejarlo demasiado tarde, para no tener que refactorizar la mitad de su motor. Por otro lado, no te involucres demasiado en la optimización de las cosas para exprimir cada ciclo si crees que podrías cambiar el algoritmo por completo mañana, o si no has puesto datos reales del juego a través de él.

Retire la fruta que cuelga a medida que avanza, aborde las cosas grandes periódicamente, y debería estar bien.

JasonD
fuente
Para agregar al generador de perfiles dentro del juego (con el que estoy totalmente de acuerdo), extender su perfil para mostrar múltiples barras (para múltiples marcos) lo ayuda a correlacionar el comportamiento del juego con los picos y puede ayudarlo a encontrar cuellos de botella que no aparecerán en su captura promedio con un perfilador.
Kaj
2

Si mira la cita de Knuth en su contexto, continúa explicando que debemos optimizar pero con herramientas, como un generador de perfiles.

Debe perfilar constantemente su perfil de memoria y su aplicación después de que se establezca la arquitectura muy básica.

La creación de perfiles no solo lo ayudará a aumentar la velocidad, sino que también lo ayudará a encontrar errores. Si su programa de repente cambia drásticamente la velocidad, esto generalmente se debe a un error. Si no está perfilando, puede pasar desapercibido.

El truco para optimizar es hacerlo por diseño. No esperes hasta el último minuto. Asegúrese de que el diseño de su programa le brinde el rendimiento que necesita sin realmente trucos desagradables del bucle interno.

Jonathan Fischoff
fuente
1

Para mi proyecto, generalmente aplico algunas optimizaciones MUY necesarias en mi motor base. Por ejemplo, siempre me gusta implementar una buena implementación SIMD sólida usando SSE2 y 3DNow. Esto asegura que mi matemática de coma flotante esté en el lugar donde quiero que esté. Otra buena práctica es aprovechar las optimizaciones a medida que codifica en lugar de retroceder. La mayoría de las veces, estas pequeñas prácticas son tan lentas como lo que estaba codificando de todos modos. Antes de codificar una función, asegúrese de investigar la forma más eficiente de hacerlo.

En pocas palabras, en mi opinión, es MÁS DIFÍCIL hacer que su código sea más eficiente después de que ya es una mierda.

Krankzinnig
fuente
0

Diría que la forma más fácil sería usar su sentido común: si algo parece que se está ejecutando lentamente, échele un vistazo. Vea si es un cuello de botella.
Use un generador de perfiles para ver las funciones de velocidad que toman y con qué frecuencia se les llama.
No tiene ningún sentido optimizar o perder tiempo tratando de optimizar algo que no lo necesita.

El pato comunista
fuente
0

Si su código se ejecuta lentamente, ejecute un generador de perfiles y vea qué es exactamente lo que hace que se ejecute más lento. O puede ser proactivo y tener un generador de perfiles ejecutándose antes de comenzar a notar problemas de rendimiento.

Querrás optimizar cuando tu velocidad de cuadros baje a un punto que el juego comience a sufrir. Su culpable más probable será que su CPU se use demasiado (100%).

Bryan Denny
fuente
Yo diría que la GPU es tan probable como la CPU. De hecho, dependiendo de cuán estrechamente acopladas estén las cosas, es completamente posible estar fuertemente vinculado a la CPU en la mitad del marco, y fuertemente a la GPU en la otra mitad. El perfil tonto puede incluso mostrar una utilización inferior al 100% en cualquiera de los dos. Asegúrese de que su perfil sea lo suficientemente detallado como para demostrarlo (¡pero no tan fino como para ser intrusivo!)
JasonD
0

Debería optimizar su código ... tan a menudo como sea necesario.

Lo que he hecho en el pasado es ejecutar continuamente el juego con el perfil activado (al menos un contador de velocidad de cuadros en la pantalla en todo momento). Si el juego se está volviendo lento (por debajo de su velocidad de fotogramas objetivo en su máquina de especificaciones mínimas, por ejemplo), encienda el generador de perfiles y vea si aparecen puntos calientes.

A veces no es el código. Muchos de los problemas con los que me he encontrado en el pasado han estado orientados a gpu (concedido, esto estaba en el iPhone). Problemas de velocidad de llenado, demasiadas llamadas de sorteo, falta de geometría suficiente, sombreadores ineficientes ...

Además de algoritmos ineficientes para problemas difíciles (es decir, búsqueda de caminos, física), rara vez me he encontrado con problemas en los que el código en sí era el culpable. Y esos problemas difíciles deberían ser cosas en las que invierte mucho esfuerzo para obtener el algoritmo correcto y no preocuparse por cosas más pequeñas.

Tétrada
fuente
0

Para mí es el mejor seguir un modelo de datos bien preparado. Y optimización-antes del principal paso adelante. Quiero decir, antes de comenzar a implementar algo grande y nuevo. Otra razón para la optimización es cuando estoy perdiendo el control de los recursos, la aplicación necesita mucha carga de CPU / GPU o memoria y no sé por qué :) o es demasiado.

samboush
fuente