Mostrando entradas con la etiqueta Optimización. Mostrar todas las entradas
Mostrando entradas con la etiqueta Optimización. Mostrar todas las entradas

viernes, 9 de septiembre de 2011

El problema "curioso" (y su resolución)

music: PJ HARVEY -"This Is Love"-

Durante las últimas semanas me he dedicado a solucionar el problema "curioso" del que ya hablaba en la entrada del día 12/05/2011.

Se hacía imprescindible poder dar una solución a esto si pretendía grabar el inplay del juego; como comentaba en la entrada anterior, era imposible hacer un vídeo que se viera decentemente debido al enorme lag que se producía.

Afortunadamente ya dejé hace tiempo las pistas necesarias por donde podía ponerme a empezar a trabajar para solventar esto... y los resultados han sido muy satisfactorios. : )

Al contrario de lo que pensaba el Componente LogicaPelota no era el problema... pero sí la puerta de entrada para ir, poco a poco, "buceando" en el código hasta llegar al núcleo principal de los problemas: los métodos IBounding::GetCollision e IBounding::GetPoints.

La optimización, realizada principalmente en dichos métodos, ha llevado a aumentar el framerate del juego (estando en modo Debug) hasta los... ¡¡¡147-150 FPS!!!. Teniendo en cuenta que, en estas mismas condiciones, las medidas marcaban 46-48 FPS ( hace tiempo hice algunas pequeñas optimizaciones que lo subieron hasta ahí desde la situación de (12/05/2011) ) el empuje ha sido espectacular. : )

Resumo las modificaciones que se han realizado para llevar a cabo la hazaña:

a) Dejar de devolver por valor un std::vector. Una de las cosas que he aprendido gracias a Quimera Engine es que, cuando devuelves por valor el resultado de un método, está sucediendo lo mismo que con los parámetros de entrada por valor: se está creando UNA COPIA del contenedor que alberga el resultado. Si dicho resultado es un std::vector, se tiene que crear UNA COPIA del vector... con lo que se estaba gastando inútilmente un montón de tiempo en cada iteración de GetPoints. Solución: poner un std::vector como un parámetro de salida en GetPoints, y por referencia.

b) Por lo que he podido ver, llamar al método std::vector.clear() en cada iteración de un método puede ser algo ASESINO. Hay que evitar a toda costa llamar a std::vector.clear() en un lugar que tiene que ejecutarse en cada iteración del juego, y con la mayor rapidez posible.

c) Asimismo, realizar múltiples llamadas a std::vector.push_back(elemento) en una misma iteración de un método (en concreto, unas 94 veces), también puede llegar a ser ASESINO. Hay que evitar realizar tantas llamadas a push_back en un lugar que tiene que ejecutarse en cada iteración del juego, y con la mayor rapidez posible.

d) La mezcla de b) y c) había dado un cóctel particularmente letal para el rendimiento. La solución ha sido sencilla:

d.1) Se fija un límite máximo de tamaño para los std::vector (en concreto, para los std::vector de puntos 3D) y se declaran ya con dicho tamaño. Así, ya no es necesario emplear ni clear para vaciarlos en cada iteración, ni push_back para añadir un elemento. Pueden ser los elementos introducidos directamente como si fueran arrays.

d.2) Al tener tamaño fijo ya no es posible emplear el método std::vector.size() para conocer cuántos elementos posee el vector. Por lo que se devolverá también, como parámetro de salida, una variable unsigned int que contendrá la cantidad de elementos que poseerá el vector. Dicho valor podrá ser empleado como centinela para recorrer el vector en un bucle for.

e) Por si fuera poco, había un fallo tonto en IBounding::Draw que estaba haciendo consumir inútilmente un montón de tiempo, y que ya ha sido subsanado.

Aparte... ciertos trucos de más bajo nivel, principalmente declaración de algunos grupos de variables locales a un método como static, que han hecho ganar algunos frames más... pero el gran avance en el rendimiento del juego ha sido gracias a lo explicado anteriormente.

La solución final es terminar de plantear e implementar el sistema de detección y respuesta de colisiones de una forma distinta a la actual, pero por el momento la optimización realizada ha sido suficiente. Al fín se ha podido grabar un vídeo. Ya veremos si continuo optimizando más o me voy a hacer otras cosas más urgentes (como introducir audio, por ejemplo).

Hasta otra. :P

sábado, 20 de agosto de 2011

Reduciendo iteraciones

music: AEROSMITH -"Beyond Beautiful"- / -"She's On Fire"-

Partamos de la siguiente base: el bloque de código más rápido es aquel que nunca se ejecuta. Con esta premisa es evidente que nos interesa lograr, de una forma u otra, reducir la cantidad de veces que ciertos bloques de código se ejecutan para mejorar el rendimiento, si ello es posible. Independientemente de lo mejor o peor implementados (y optimizados) que estén dichos bloques.

Una de las razones por las que no he podido llegar a mucho en términos de optimización es porque concebía la optimización exclusivamente como la "aceleración" de la ejecución de ciertas partes de código particularmente costosas computacionalmente, concentrándome demasiado en este enfoque. No se trata de olvidar esto ni mucho menos, sino de anteponer una política de reducción de iteraciones en ciertos puntos clave. Y en el caso de Super Pong, estos puntos clave son los Componentes.

Existen compomentes que no necesitan ser ejecutados todo el tiempo, en cada tick (la IA o la visión de la raqueta, por ejemplo). Desgraciadamente, el engine no estaba preparado para poder establecer una política así, en la que se establezca que un determinado componente no va a ejecutar su método Update en cada iteración, sino sólamente cada vez que haya transcurrido un determinado espacio de tiempo. Y eso es a lo que me he estado dedicando durante los últimos días.

El punto clave de todo esto es la clase IComponent, de la cual heredan TODAS las demás clases componente. El siguiente resúmen explica los puntos más importantes:

AÑADIDOS en IComponent:

Atributo m_fElapsedTime_Max: El Elapsed Time Establecido para el compoente (en ms, pero expresado en segundos). Representa el intervalo de tiempo que debe transcurrir para dar permiso al componente para ejecutar su método Update_Exec. Se asignará en TODOS los componentes un valor para m_fElapsedTime_Max en el método Init del componente. Asignando 0.0f vamos a hacer hacemos que el componente ejecute Update_Exece en cada tick.

Atributo m_fElapsedTime_Acum: El Elapsed Time (en ms, pero expresado en segundos) actualmente acumulado en el componente. Cuando se alcance el Elapsed Time Establecido, se ejecutará el método Update_Exec del componente.

Método virtual Update_Exec() vacío. Este será, como tal, el antiguo método Update. Todos los componentes heredarán este método vacío, sin código; se sobreescribirá en TODOS los componentes (salvo en aquellos que no tengan elementos que actualizar), y se ejecutará cuando se haya alcanzado el Elapsed Time Establecido para el componente.

MODIFICACIONES en IComponent:

IComponent::Update deja de ser un método virtual vació para pasar a ser no virtual y albergar el bloque de código que comprueba que, si se ha alcanzado el Elapsed Time establecido para ese componente, se ordene ejecutar sú método Update_Exec.

(** TO-DO: ya me acordaré algún día de cómo se tabulaba esto... **)

void IComponent::Update(const float& fDelta)
{
m_fElapsedTime_Acum += fDelta;
if ( m_fElapsedTime_Acum >= m_fElapsedTime_Max )
{
m_fElapsedTime_Acum = 0.0f;
this->Update_Exec(fDelta);
}
}

Este método es heredado por todos los componentes, y cuando la Entidad ordena a sus componentes que se actualicen (manda hacer Update a todos sus componentes, vamos), está llamando a este Update heredado de IComponent.

Tras esto, se puede comenzar a pensar un plan de "presupuestos" (Budget es la palabra) de tiempo para cada componente... y el engine está preparado. Por el momento, tengo establecido que los componentes Vista e IANPC ejecuten el método Update_Exec cada 500 ms (sus m_fElapsedTime_Max tendrán almacenado 0.5f, al estar expresado en segundos).

Hice una prueba loca y asigné 10 segundos a los componentes Vista e IANPC, y el framerate no aumentó respecto del caso anterior. Pero hice una segunda prueba loca asignando también 10 segundos al componente LógicaPelota... ¡¡¡y el framerate aumentó en casi VEINTE FRAMES!!!. El componente LogicaPelota, por lo tanto, está suponiendo un cuello de botella importante (como ya sospechaba). Habrá que optimizarlo acelerando su ejecución... y antes de ello reduciendo el número de iteraciones. ; )

Hasta otra. :P

martes, 25 de mayo de 2010

Probando nVidia PerfHUD

music: BLUE ÖYSTER CULT -"See You In Black"-

En estos días he estado probando nVidia PerfHUD (versión 6.62), un programa tipo profiler que puede ayudar a detectar cuellos de botella en el pipeline gráfico. Quiero ver si la caída en el framerate que comenté en la anterior entrada tuviera también algo que ver con un mal uso de Direct3D.

De momento no he tenido mucho éxito. Durante la instalación, se me indica que me van a ser actualizados los drivers de la tarjeta gráfica... el problema es que mi tarjeta está ahora mismo un poco anticuada y no pasa la prueba del logotipo de Windows con esos últimos drivers con lo que el asunto, como puede esperarse, no pinta muy bien. Y así ha sido: el programa se cuelga cada dos por tres, y el ordenador se ha quedado algo inestable hasta que le he vuelto a reinstalar los drivers anteriormente instalados.

Total, que voy a probar con una versión más antigua, a ver si así hay más suerte.

Hasta otra. :P

(** EDIT 31/05/2010 **) Ídem de ídem con la versión 5.2, y ya no se ven versiones más antiguas del entorno. Así que este asunto, al menos por el momento, así se va a quedar... :(