Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Consejos generales de optimización

Introducción

En un mundo ideal, las computadoras funcionarían a una velocidad infinita. El único límite para lo que podríamos lograr sería nuestra imaginación. Sin embargo, en el mundo real, es muy fácil producir software que incluso la computadora más rápida no pueda manejar.

Por lo tanto, el diseño de juegos y otro software es un compromiso entre lo que nos gustaría que fuera posible y lo que podemos lograr de manera realista, manteniendo un buen rendimiento.

Para alcanzar los mejores resultados, tenemos dos enfoques:

  • Trabaja más rápido.

  • Trabaja de manera más inteligente.

Y preferiblemente, utilizaremos una combinación de ambos.

Humo y espejos

Parte de trabajar de manera más inteligente es reconocer que, en los juegos, a menudo podemos lograr que el jugador crea que está en un mundo mucho más complejo, interactivo y visualmente emocionante de lo que realmente es. Un buen programador es un mago y debe esforzarse por aprender los trucos del oficio mientras intenta inventar nuevos.

La naturaleza de la lentitud

Para un observador externo, los problemas de rendimiento a menudo se agrupan juntos. Pero en realidad, existen varios tipos diferentes de problemas de rendimiento:

  • Un proceso lento que ocurre en cada cuadro, lo que resulta en una velocidad de cuadros continuamente baja.

  • Un proceso intermitente que causa "picos" de lentitud, lo que resulta en bloqueos.

  • Un proceso lento que ocurre fuera del juego normal, por ejemplo, durante la carga de un nivel.

Cada uno de estos es molesto para el usuario, pero de diferentes maneras.

Medición de desempeño

Probablemente, la herramienta más importante para la optimización es la capacidad de medir el rendimiento, identificar los cuellos de botella y medir el éxito de nuestros intentos por acelerarlos.

Existen varios métodos para medir el desempeño, que incluyen:

Ten en cuenta que el rendimiento relativo de diferentes áreas puede variar en diferentes hardware. A menudo es una buena idea medir los tiempos en más de un dispositivo. Esto es especialmente cierto si estás apuntando a dispositivos móviles.

Limitaciones

Los perfiles de CPU suelen ser el método preferido para medir el rendimiento. Sin embargo, no siempre cuentan toda la historia.

  • A menudo, los cuellos de botella se encuentran en la GPU como resultado de las instrucciones dadas por la CPU.

  • Las fluctuaciones pueden ocurrir en los procesos del sistema operativo (fuera de Godot) como resultado de las instrucciones utilizadas en Godot (por ejemplo, asignación dinámica de memoria).

  • Es posible que no siempre puedas perfilar dispositivos específicos, como un teléfono móvil, debido a la configuración inicial requerida.

  • Es posible que tengas que resolver problemas de rendimiento que ocurren en hardware al cual no tienes acceso.

Como resultado de estas limitaciones, a menudo necesitas realizar una labor detectivesca para descubrir dónde se encuentran los cuellos de botella.

trabajo de detective

La labor detectivesca es una habilidad crucial para los desarrolladores, tanto en términos de rendimiento como de solución de errores. Esto puede incluir pruebas de hipótesis y búsqueda binaria.

Evaluación de la hipótesis

Por ejemplo, supongamos que crees que los sprites están ralentizando tu juego. Puedes poner a prueba esta hipótesis mediante:

  • Medir el rendimiento al agregar más sprites o eliminar algunos.

Esto puede llevar a una hipótesis adicional: ¿el tamaño del sprite determina la disminución en el rendimiento?

  • Puedes poner a prueba esto manteniendo todo igual, pero cambiando el tamaño del sprite y midiendo el rendimiento.

Perfiladores

Los perfiles de rendimiento te permiten medir el tiempo que tarda tu programa mientras se ejecuta. Los perfiles de rendimiento proporcionan resultados que indican el porcentaje de tiempo que se gastó en diferentes funciones y áreas, y con qué frecuencia se llamaron las funciones.

Esto puede ser muy útil tanto para identificar cuellos de botella como para medir los resultados de tus mejoras. A veces, los intentos de mejorar el rendimiento pueden tener el efecto contrario y llevar a un rendimiento más lento. Siempre utiliza perfiles de rendimiento y mediciones de tiempo para orientar tus esfuerzos.

For more info about using Godot's built-in profiler, see The Profiler.

Principios

Donald Knuth dijo:

Los programadores desperdician enormes cantidades de tiempo pensando o preocupándose por la velocidad de partes no críticas de sus programas, y estos intentos de eficiencia en realidad tienen un fuerte impacto negativo cuando se considera la depuración y el mantenimiento. Deberíamos olvidarnos de las eficiencias pequeñas, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todos los males. Sin embargo, no debemos dejar pasar nuestras oportunidades en ese 3% crítico.

Los mensajes son muy importantes:

  • El tiempo de los desarrolladores es limitado. En lugar de intentar acelerar ciegamente todos los aspectos de un programa, debemos concentrar nuestros esfuerzos en los aspectos que realmente importan.

  • Los esfuerzos de optimización a menudo resultan en código más difícil de leer y depurar que el código no optimizado. Nos interesa limitar esto a las áreas que realmente se beneficiarán.

Solo porque podamos optimizar una parte particular del código, no significa necesariamente que debamos hacerlo. Saber cuándo y cuándo no optimizar es una gran habilidad que debemos desarrollar.

Una aspecto engañoso de la cita es que las personas tienden a centrarse en la subcita "la optimización prematura es la raíz de todos los males". Si bien la optimización prematura (por definición) no es deseable, el software de alto rendimiento es el resultado de un diseño eficiente.

Diseño eficaz

El peligro de animar a las personas a ignorar la optimización hasta que sea necesaria es que convenientemente se pasa por alto que el momento más importante para considerar el rendimiento es en la etapa de diseño, incluso antes de que se presione una tecla en el teclado. Si el diseño o los algoritmos de un programa son ineficientes, entonces no importa cuánto se pulan los detalles más tarde, nunca funcionará tan rápido como un programa diseñado para el rendimiento desde el principio.

Esto tiende a ser mucho más importante en la programación de juegos o gráficos que en la programación en general. Un diseño eficiente, incluso sin optimización a nivel de bajo nivel, a menudo se ejecutará muchas veces más rápido que un diseño mediocre con optimización a nivel de bajo nivel.

Diseño incremental

Por supuesto, en la práctica, a menos que tengas conocimientos previos, es poco probable que encuentres el mejor diseño la primera vez. En cambio, a menudo crearás una serie de versiones de una área específica de código, cada una abordando el problema de manera diferente, hasta que llegues a una solución satisfactoria. Es importante no dedicar demasiado tiempo a los detalles en esta etapa hasta que hayas finalizado el diseño general. De lo contrario, gran parte de tu trabajo podría ser descartado.

Es difícil establecer pautas generales para un diseño eficiente, ya que esto depende mucho del problema en cuestión. Sin embargo, vale la pena mencionar un punto en el lado de la CPU: los CPU modernos casi siempre se ven limitados por el ancho de banda de memoria. Esto ha llevado a un resurgimiento en el diseño orientado a datos, que implica diseñar estructuras de datos y algoritmos para tener localidad de caché de datos y acceso lineal, en lugar de saltar por la memoria.

El proceso de optimización

Suponiendo que tenemos un diseño razonable y tomando lecciones de Knuth, nuestro primer paso en la optimización debería ser identificar los cuellos de botella más grandes: las funciones más lentas, el fruto más bajo colgante.

Una vez que hayamos mejorado con éxito la velocidad del área más lenta, es posible que ya no sea el cuello de botella. Por lo tanto, debemos probar o perfilar nuevamente y encontrar el siguiente cuello de botella en el que centrarnos.

El proceso es el siguiente:

  1. Perfilar / Identificar el cuello de botella.

  2. Optimiza cuellos de botella.

  3. Vuelve al paso 1.

Optimización de cuellos de botella

Algunos perfiles de rendimiento incluso te indicarán qué parte de una función (accesos a datos, cálculos) está ralentizando las cosas.

Al igual que con el diseño, debes concentrar tus esfuerzos primero en asegurarte de que los algoritmos y las estructuras de datos sean lo mejor posible. El acceso a los datos debe ser local (para aprovechar al máximo la memoria caché de la CPU), y a menudo es mejor utilizar un almacenamiento compacto de datos (nuevamente, siempre realiza perfiles de rendimiento para probar los resultados). Con frecuencia, se precalculan las operaciones intensivas con anticipación. Esto se puede hacer al cargar un nivel, al cargar un archivo que contenga datos precalculados o simplemente almacenando los resultados de cálculos complejos en una constante de script y leyendo su valor.

Una vez que los algoritmos y los datos son buenos, a menudo se pueden realizar pequeños cambios en las rutinas que mejoran el rendimiento. Por ejemplo, puedes mover algunos cálculos fuera de los bucles o transformar bucles for anidados en bucles no anidados. (Esto debería ser factible si conoces de antemano el ancho o alto de una matriz 2D)

Siempre vuelve a realizar pruebas de tiempo y busca cuellos de botella después de cada cambio que realices. Algunos cambios aumentarán la velocidad, otros pueden tener un efecto negativo. A veces, un pequeño efecto positivo será contrarrestado por los aspectos negativos de un código más complejo, y es posible que decidas dejar fuera esa optimización.

Apendice

Cuello de botella matematico

El proverbio "una cadena es tan fuerte como su eslabón más débil" se aplica directamente a la optimización de rendimiento. Si tu proyecto está gastando el 90% del tiempo en la función "A", optimizar "A" puede tener un efecto masivo en el rendimiento.

A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms

En este ejemplo, mejorar este cuello de botella "A" en un factor de 9× disminuye el tiempo total del fotograma en un factor de 5× al tiempo que aumenta los fotogramas por segundo en 5×.

Sin embargo, si algo más se está ejecutando lentamente y también está limitando tu proyecto, la misma mejora puede llevar a ganancias menos drásticas:

A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms

En este ejemplo, aunque hayamos optimizado enormemente la función A, la ganancia real en términos de frecuencia de fotogramas es bastante pequeña.

En los juegos, las cosas se vuelven aún más complicadas porque la CPU y la GPU se ejecutan de forma independiente. El tiempo total de fotograma se determina por el componente más lento de los dos, ya sea la CPU o la GPU.

CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms

En este ejemplo, optimizamos enormemente la CPU una vez más, pero el tiempo de fotograma no mejoró porque estamos limitados por la GPU.