Generación de perfiles de memoria en Unity
Al perfilar y perfeccionar el rendimiento de su juego para una amplia gama de plataformas y dispositivos, puede expandir su base de jugadores y aumentar sus posibilidades de éxito.
Esta página proporciona información sobre dos herramientas para analizar el uso de memoria en tu aplicación en Unity: el módulo integrado Memory Profiler y el paquete Memory Profiler, un paquete de Unity que puedes agregar a tu proyecto.
La información aquí es extraída del libro electrónico, Guía definitiva para perfilar juegos de Unity, disponible para descargar de forma gratuita. El libro electrónico fue creado por expertos externos e internos de Unity en desarrollo, creación de perfiles y optimización de juegos.
Sigue leyendo para obtener más información sobre la creación de perfiles de memoria en Unity.
La generación de perfiles de memoria es útil para realizar pruebas con limitaciones de memoria de la plataforma de hardware, reducir el tiempo de carga y los bloqueos, y hacer que el proyecto sea compatible con dispositivos más antiguos. También puede ser relevante si desea mejorar el rendimiento de la CPU/GPU realizando cambios que realmente aumenten el uso de la memoria. En gran medida, no está relacionado con el rendimiento en tiempo de ejecución.
Hay dos formas de analizar el uso de memoria en tu aplicación en Unity.
El módulo Memory Profiler: Se trata de un módulo de generador de perfiles integrado que proporciona información básica sobre dónde usa la memoria la aplicación.
El paquete del Generador de perfiles de memoria: Este es un paquete de Unity que puedes agregar a tu proyecto. Agrega una ventana adicional de Memory Profiler al Editor de Unity, que luego puede usar para analizar el uso de memoria en su aplicación con aún más detalle. Puedes almacenar y comparar imágenes para encontrar filtraciones de la memoria o ver el diseño de la memoria para identificar problemas de fragmentación.
Con estas herramientas integradas, puede supervisar el uso de la memoria, localizar áreas de una aplicación en las que el uso de la memoria es mayor de lo esperado y encontrar y mejorar la fragmentación de la memoria.
Comprender y presupuestar las limitaciones de memoria de los dispositivos de destino es fundamental para el desarrollo multiplataforma. Al diseñar escenas y niveles, apéguese al presupuesto de memoria establecido para cada dispositivo de destino. Al establecer límites y directrices, puede asegurarse de que su aplicación funcione bien dentro de los límites de las especificaciones de hardware de cada plataforma.
Puede encontrar las especificaciones de la memoria del dispositivo en la documentación para desarrolladores. Por ejemplo, la consola Xbox One está limitada a 5 GB de memoria máxima disponible para los juegos que se ejecutan en primer plano, según la documentación.
También puede ser útil establecer presupuestos de contenido en torno a la complejidad de la malla y el sombreador, así como para la compresión de texturas. Todo esto influye en la cantidad de memoria que se asigna. Estas cifras presupuestarias se pueden consultar durante el ciclo de desarrollo del proyecto.
Determinación de los límites de RAM física
Cada plataforma de destino tiene un límite de memoria y, una vez que lo sepa, puede establecer un presupuesto de memoria para la aplicación. Utilice el Generador de perfiles de memoria para ver una instantánea de captura. Los recursos de hardware (consulte la imagen anterior) muestran los tamaños de memoria física de acceso aleatorio (RAM) y memoria de acceso aleatorio de vídeo (VRAM). Esta cifra no tiene en cuenta el hecho de que no todo ese espacio podría estar disponible para su uso. Sin embargo, proporciona una cifra aproximada útil para comenzar a trabajar.
Es una buena idea hacer una referencia cruzada de las especificaciones de hardware para las plataformas de destino, ya que es posible que las cifras que se muestran aquí no siempre muestren el panorama completo. El hardware del kit de desarrollo a veces tiene más memoria, o puede estar trabajando con hardware que tiene una arquitectura de memoria unificada.
Identifique el hardware con la especificación más baja en términos de RAM para cada plataforma que admita y utilícelo para guiar su decisión de presupuesto de memoria. Recuerde que es posible que no toda esa memoria física esté disponible para su uso. Por ejemplo, una consola podría tener un hipervisor ejecutándose para admitir juegos más antiguos que podrían usar parte de la memoria total. Piense en un porcentaje (por ejemplo, el 80% del total) para usar. En el caso de las plataformas móviles, también puede considerar la posibilidad de dividirse en varios niveles de especificaciones para admitir una mejor calidad y funciones para aquellos con dispositivos de gama alta.
Una vez que haya definido un presupuesto de memoria, considere la posibilidad de establecer presupuestos de memoria por equipo. Por ejemplo, los artistas del entorno obtienen una cierta cantidad de memoria para usar en cada nivel o escena que se carga, el equipo de audio obtiene la asignación de memoria para la música y los efectos de sonido, etc.
Es importante ser flexible con los presupuestos a medida que avanza el proyecto. Si un equipo está muy por debajo del presupuesto, asigna el excedente a otro equipo si puede mejorar las áreas del juego que están desarrollando.
Una vez que decidas y establezcas los presupuestos de memoria para tus plataformas de destino, el siguiente paso es usar herramientas de generación de perfiles para ayudarte a supervisar y realizar un seguimiento del uso de memoria en tu juego.
El módulo Generador de perfiles de memoria proporciona dos vistas: Sencillo y detallado. Use la vista simple para obtener una vista de alto nivel del uso de memoria de la aplicación. Cuando sea necesario, cambie a la vista Detallada para profundizar más.
Simple
La cifra de memoria reservada total es el "Total rastreado por la memoria de Unity". Incluye la memoria que Unity ha reservado pero que no está usando en este momento (esa cifra es la memoria total utilizada).
La cifra de memoria usada por el sistema es lo que el sistema operativo considera que está en uso por parte de la aplicación. Si esta cifra muestra alguna vez 0, tenga en cuenta que esto indica que el contador del generador de perfiles no está implementado en la plataforma de la que está generando perfiles. En este caso, el mejor indicador en el que confiar es la memoria reservada total. También se recomienda cambiar a una herramienta de generación de perfiles de plataforma nativa para obtener información detallada de la memoria en estos casos.
Para ver cuánta memoria usan el ejecutable, los archivos DLL y la máquina virtual Mono, las cifras de memoria fotograma a fotograma no son suficientes. Utilice una captura de instantánea detallada para profundizar en este tipo de desglose de memoria.
*NOTA*: El árbol de referencia de la vista detallada del módulo Generador de perfiles de memoria solo muestra referencias nativas. Las referencias de objetos de tipos que heredan de UnityEngine.Object pueden aparecer con el nombre de sus shells administrados. Sin embargo, es posible que aparezcan solo porque tienen objetos nativos debajo de ellos. No verá necesariamente ningún tipo administrado. Tomemos como ejemplo un objeto con un Texture2Din en uno de sus campos como referencia. Con esta vista, tampoco verá qué campo contiene esa referencia. Para este tipo de detalles, utilice el paquete de generador de perfiles de memoria.
Para determinar a grandes rasgos cuándo el uso de memoria comienza a acercarse a los presupuestos de la plataforma, use el siguiente cálculo del reverso de la servilleta:
Memoria utilizada por el sistema (o memoria reservada total si el sistema utilizado muestra 0) + búfer aproximado de memoria sin seguimiento / memoria total de la plataforma
Cuando esta cifra comience a acercarse al 100 % del presupuesto de memoria de la plataforma, use el paquete Memory Profiler para averiguar por qué.
Muchas de las características del módulo Memory Profiler han sido reemplazadas por el paquete Memory Profiler, pero aún puede usar el módulo para complementar sus esfuerzos de análisis de memoria.
Por ejemplo:
- Para detectar asignaciones de GC: Aunque estos aparecen en el módulo, son más fáciles de rastrear utilizando Project Auditor o Deep Profiling.
- Para ver rápidamente el tamaño usado/reservado del montón
- Análisis de memoria de sombreador
Recuerde crear un perfil en el dispositivo que tenga las especificaciones más bajas para su plataforma de destino general al establecer un presupuesto de memoria. Supervise de cerca el uso de la memoria, teniendo en cuenta los límites de su objetivo.
Por lo general, querrá crear un perfil utilizando un sistema de desarrollo potente con mucha memoria disponible (es importante el espacio para almacenar instantáneas de memoria grande o cargar y guardar esas instantáneas rápidamente).
La generación de perfiles de memoria es una bestia diferente en comparación con la generación de perfiles de CPU y GPU, ya que puede incurrir en una sobrecarga de memoria adicional. Es posible que tenga que generar perfiles de memoria en dispositivos de gama alta (con más memoria), pero tenga en cuenta específicamente el límite de presupuesto de memoria para la especificación de destino de gama baja.
Puntos que se deben tener en cuenta al generar perfiles para el uso de memoria:
- La configuración, como los niveles de calidad, los niveles de gráficos y las variantes de AssetBundle, puede tener un uso de memoria diferente en dispositivos más potentes. Por ejemplo:
- La configuración de Nivel de calidad y Gráficos podría afectar al tamaño de RenderTextures utilizado para los mapas de sombras.
- El escalado de resolución podría afectar al tamaño de los búferes de pantalla, RenderTextures y los efectos de posprocesamiento.
- La configuración de la calidad de la textura podría afectar el tamaño de todas las texturas.
- El nivel de detalle máximo podría afectar a los modelos y más.
- Si tiene variantes de AssetBundle como una versión HD (alta definición) y una versión SD (definición estándar) y elige cuál usar en función de las especificaciones del dispositivo, también puede obtener diferentes tamaños de activos según el dispositivo en el que esté perfilando.
- La resolución de pantalla de su dispositivo de destino afectará el tamaño de RenderTextures utilizado para los efectos de posprocesamiento.
- La API de gráficos admitida de un dispositivo puede afectar al tamaño de los sombreadores en función de las variantes de ellos compatibles o no con la API.
- Tener un sistema escalonado que utilice diferentes configuraciones de calidad, configuraciones de nivel gráfico y variaciones de paquetes de activos es una excelente manera de poder dirigirse a una gama más amplia de dispositivos, por ejemplo, cargando una versión de alta definición de un AssetBundle en un dispositivo móvil de 4 GB y una versión de definición estándar en un dispositivo de 2 GB. Sin embargo, tenga en cuenta las variaciones anteriores en el uso de la memoria y asegúrese de probar ambos tipos de dispositivos, así como los dispositivos con diferentes resoluciones de pantalla o API de gráficos compatibles.
*NOTA*: Por lo general, el Editor de Unity siempre mostrará una huella de memoria más grande debido a los objetos adicionales que se cargan desde el Editor y el Generador de perfiles. Incluso puede mostrar la memoria de activos que no se cargaría en la memoria en una compilación, como desde paquetes de activos (según el modo de simulación de direccionables) o sprites y atlas, o para los activos que se muestran en el inspector. Algunas de las cadenas de referencia también pueden ser más confusas en el Editor.
El Memory Profiler se encuentra actualmente en versión preliminar para Unity 2019 LTS o posterior, pero se espera que se verifique en Unity 2022 LTS.
Una gran ventaja del paquete Memory Profiler es que, además de capturar objetos nativos (como lo hace el módulo Memory Profiler), también le permite ver la memoria administrada, guardar y comparar instantáneas y explorar el contenido de la memoria con aún más detalle, con desgloses visuales del uso de la memoria.
Una instantánea muestra las asignaciones de memoria en el motor, lo que le permite identificar rápidamente las causas del uso excesivo o innecesario de la memoria, realizar un seguimiento de las pérdidas de memoria o ver la fragmentación del montón.
Después de instalar el paquete Memory Profiler, ábralo haciendo clic en Ventana > Análisis > Memory Profiler.
La barra de menú superior del Generador de perfiles de memoria le permite cambiar el destino de selección del reproductor y capturar o importar instantáneas.
*NOTA*: Genere perfiles de memoria en el hardware de destino conectando el Generador de perfiles de memoria al dispositivo remoto con el menú desplegable Selección de destino. La generación de perfiles en el Editor de Unity le dará cifras inexactas debido a los gastos generales agregados por el Editor y otras herramientas.
A la izquierda de la ventana del Generador de perfiles de memoria se encuentra el área de Workbench. Utilícelo para administrar y abrir o cerrar instantáneas de memoria guardadas. También puede utilizar esta área para cambiar entre las vistas Instantáneas únicas y Comparar.
Al igual que el Analizador de perfiles, el Generador de perfiles de memoria permite cargar dos conjuntos de datos (instantáneas de memoria) para compararlos. Esto es especialmente útil cuando se observa cómo creció el uso de memoria a lo largo del tiempo o entre escenas y cuando se buscan pérdidas de memoria.
Memory Profiler tiene una serie de pestañas en la ventana principal que le permiten profundizar en instantáneas de memoria, incluidas Resumen, Objetos y asignaciones, y Fragmentación. Veamos cada una de estas opciones en detalle.
Elija esta vista cuando desee obtener una descripción general rápida del uso de memoria de un proyecto. También contiene cifras útiles e importantes relacionadas con la memoria para la instantánea de memoria capturada en cuestión. Es perfecto para echar un vistazo rápido a lo que está sucediendo en el momento en que se tomó una instantánea.
La vista Mapa de árbol muestra un desglose de la memoria utilizada por los objetos como un mapa de árbol gráfico en el que puede profundizar para descubrir el tipo de objetos que consumen más memoria.
Debajo de la vista Mapa de árbol hay una tabla filtrada que se actualiza para mostrar la lista de objetos en las celdas de cuadrícula seleccionadas.
El mapa de árbol muestra la memoria atribuida a los objetos, ya sea nativa o administrada. La memoria de objetos administrados tiende a eclipsarse con la memoria de objetos nativos, lo que dificulta su detección en la vista de mapa. Puede hacer zoom en el mapa de árbol para verlos, pero para inspeccionar objetos más pequeños, las tablas suelen proporcionar una mejor visión general. Al hacer clic en las celdas del mapa de árbol, se filtrará la tabla debajo del tipo de sección y/o se seleccionará el objeto específico de interés en la tabla.
Puede realizar un seguimiento de los elementos que hacen referencia a objetos de esta lista y, posiblemente, de los campos de clase administrada en los que residen estas referencias, seleccionando la fila de la tabla o la celda de cuadrícula del mapa de árbol que la representa y, a continuación, comprobando la sección Referencias en el panel lateral Detalles. Si el lado está oculto, puede hacerlo visible a través de un botón de alternancia en la parte superior derecha de la barra de herramientas de la ventana.
*NOTA*: El mapa de árbol solo muestra objetos en la memoria. No es una representación completa de la memoria rastreada. Es importante comprender esto en caso de que observe que los números de descripción general del uso de memoria no son los mismos que el total de memoria rastreada.
Esto se debe al hecho de que no toda la memoria nativa está vinculada a objetos. También puede constar de asignaciones nativas no asociadas a objetos, como ejecutables y archivos DLL, NativeArrays, etc. Incluso conceptos más abstractos como "Espacio de memoria reservado pero no utilizado" pueden influir en el total de asignaciones nativas.
La vista Objetos y asignaciones muestra una tabla que se puede cambiar para filtrar en función de las selecciones ya hechas, como Todos los objetos, Todos los objetos nativos, Todos los objetos administrados, Todas las asignaciones nativas, etc.
Puede cambiar la tabla inferior para mostrar los objetos, las asignaciones o las regiones de memoria en el rango seleccionado. Como se indica para la vista Mapa de árbol, no toda la memoria está asociada a objetos, por lo que las páginas Todas las regiones de memoria y Todas las asignaciones nativas pueden proporcionar una imagen más completa del uso de la memoria, donde las regiones de memoria también incluyen la memoria reservada pero no utilizada actualmente.
Utilícelo a su favor cuando optimice el uso de la memoria y trate de empaquetar la memoria de forma más eficiente para las plataformas de hardware en las que los presupuestos de memoria son limitados.
Cargue una instantánea de Memory Profiler y recorra la vista Mapa de árbol para inspeccionar las categorías, ordenadas de mayor a menor tamaño de superficie de memoria.
Los recursos del proyecto suelen ser los mayores consumidores de memoria. Con la vista Tabla, busque objetos de textura, mallas, AudioClips, RenderTextures, sombreadores y búferes preasignados. Todos estos son buenos candidatos para la optimización de la memoria.
Una pérdida de memoria suele ocurrir cuando:
- Un objeto no se libera manualmente de la memoria a través del código
- Un objeto permanece en la memoria debido a una referencia no intencionada
El modo de comparación del generador de perfiles de memoria puede ayudar a encontrar pérdidas de memoria mediante la comparación de dos instantáneas durante un período de tiempo específico.
Un escenario común de pérdida de memoria en los juegos de Unity puede ocurrir después de descargar una escena.
El paquete Memory Profiler tiene un flujo de trabajo que le guía a través del proceso de detección de estos tipos de fugas mediante el modo de comparación.
A través de la comparación diferencial de varias instantáneas de memoria, puede identificar el origen de las asignaciones de memoria continuas durante la vida útil de la aplicación.
En las secciones siguientes se enumeran algunas sugerencias para ayudar a identificar las asignaciones de montón administradas en los proyectos.
El módulo Memory Profiler en Unity Profiler representa las asignaciones administradas por trama con una línea roja. Debería ser 0 la mayor parte del tiempo, por lo que los picos en esa línea indican los fotogramas que debe investigar para las asignaciones administradas.
La vista de escala de tiempo del módulo Generador de perfiles de uso de CPU muestra las asignaciones, incluidas las administradas, en rosa, lo que facilita su visualización y perfeccionamiento.
Las pilas de llamadas de asignación proporcionan una manera rápida de detectar asignaciones de memoria administrada en el código. Estos proporcionarán el detalle de la pila de llamadas que necesita con menos sobrecarga en comparación con lo que normalmente agregaría la generación de perfiles profundos, y se pueden habilitar sobre la marcha mediante el generador de perfiles estándar.
Las pilas de llamadas de asignación están deshabilitadas de forma predeterminada en el generador de perfiles. Para habilitarlos, haga clic en el botón Pilas de llamadas en la barra de herramientas principal de la ventana del Generador de perfiles. Cambie la vista Detalles a Datos relacionados.
*NOTA*: Si usas una versión anterior de Unity (antes de la compatibilidad con la pila de llamadas de asignación), la generación de perfiles profundos es una buena manera de obtener pilas de llamadas completas para ayudar a encontrar asignaciones administradas.
GC. Las muestras de asignación seleccionadas en la jerarquía o en la jerarquía sin procesar ahora contendrán sus pilas de llamadas. También puede ver las pilas de llamadas de GC. Asigna muestras en la información sobre herramientas de selección de la línea de tiempo.
La vista Jerarquía del Generador de perfiles de uso de CPU le permite hacer clic en los encabezados de columna para usarlos como criterios de ordenación. Ordenar por asignación de GC es una excelente manera de concentrarse en ellos.
Project Auditor es una herramienta experimental de análisis estático. Hace muchas cosas útiles, varias de las cuales están fuera del alcance de esta guía, pero puede producir una lista de cada línea de código en un proyecto que provoca una asignación administrada, sin tener que ejecutar el proyecto. Es una forma muy eficiente de encontrar e investigar este tipo de problemas.
Unity usa el recolector de basura Boehm-Demers-Weiser, que deja de ejecutar el código de su programa y solo reanuda la ejecución normal una vez que se completa su trabajo.
Tenga en cuenta las asignaciones de montón innecesarias que pueden causar picos de GC.
- Instrumentos de cuerda: En C#, las cadenas son tipos de referencia, no tipos de valor. Esto significa que cada nueva cadena se asignará en el montón administrado, incluso si solo se usa temporalmente. Reduzca la creación o manipulación innecesaria de cadenas. Evite analizar archivos de datos basados en cadenas, como JSON y XML, y almacene los datos en ScriptableObjects o formatos como MessagePack o Protobuf en su lugar. Use la clase StringBuilder si necesita compilar cadenas en tiempo de ejecución.
- Llamadas a la función de Unity: Algunas funciones de la API de Unity crean asignaciones de montón, particularmente las que devuelven una matriz de objetos administrados. Almacene en caché las referencias a matrices en lugar de asignarlas en medio de un bucle. Además, aprovecha ciertas funciones que evitan generar basura. Por ejemplo, use GameObject.CompareTag en lugar de comparar manualmente una cadena con GameObject.tag (ya que devolver una nueva cadena crea basura).
- Boxeo: Evite pasar una variable con tipo de valor en lugar de una variable con tipo de referencia. Esto crea un objeto temporal, y la basura potencial que viene con él convierte implícitamente el tipo de valor en un objeto de tipo (por ejemplo, int i = 123; object o = i). En su lugar, intente proporcionar invalidaciones concretas con el tipo de valor que desea pasar. Los genéricos también se pueden usar para estas invalidaciones.
- Corrutinas: Aunque yield no produce basura, la creación de un nuevo objeto WaitForSeconds sí lo hace. Almacene en caché y reutilice el objeto WaitForSeconds en lugar de crearlo en la línea yield o use yield return null.
- LINQ y expresiones regulares: Ambos generan basura a partir del boxeo detrás de escena. Evite LINQ y las expresiones regulares si el rendimiento es un problema. Escriba bucles for y use listas como alternativa a la creación de nuevas matrices.
- Colecciones genéricas y otros tipos administrados: No declares ni rellenes una lista o colección en cada fotograma de Actualización (por ejemplo, una lista de enemigos dentro de un radio determinado del jugador). En su lugar, haga que la lista sea un miembro de MonoBehaviour e inicialícela en Inicio. Simplemente vacíe la colección con Borrar cada fotograma antes de usarla.
Cronometrar la recolección de elementos no utilizados siempre que sea posible
Si está seguro de que una inmovilización de recolección de elementos no utilizados no afectará a un punto específico del juego, puede desencadenar la recolección de elementos no utilizados con System.GC.Collect.
Consulte Descripción de la administración automática de memoria para obtener ejemplos de cómo usar esto en su beneficio.
Use el recolector de elementos no utilizados incremental para dividir la carga de trabajo de GC
En lugar de crear una interrupción única y larga durante la ejecución del programa, la recolección incremental de elementos no utilizados utiliza varias interrupciones más cortas que distribuyen la carga de trabajo en muchos fotogramas. Si la recolección de elementos no utilizados provoca una velocidad de fotogramas irregular, pruebe esta opción para ver si puede reducir el problema de los picos de GC. Utilice Profile Analyzer para comprobar las ventajas que tiene para la aplicación.
Tenga en cuenta que el uso de GC en modo incremental agrega barreras de lectura y escritura a algunas llamadas de C#, lo que conlleva cierta sobrecarga que puede agregar hasta ~1 ms por fotograma de sobrecarga de llamadas de scripting. Para obtener un rendimiento óptimo, lo ideal es no tener asignaciones de GC en los bucles de juego principales para que no necesite el GC incremental para una velocidad de fotogramas fluida y pueda ocultar el GC. Recopile donde un usuario no lo note, por ejemplo, al abrir el menú o cargar un nuevo nivel.
Para obtener más información sobre el Generador de perfiles de memoria, consulte los siguientes recursos:
- Documentación del Generador de perfiles de memoria
- Mejorar el uso de memoria con el tutorial Memory Profiler en Unity
- Memory Profiler La herramienta para solucionar problemas relacionados con la memoria Sesión de Unite
- Trabajar con el generador de perfiles de memoria Sesión de Unity Learn
Descarga gratis el libro electrónico Guía definitiva para perfilar los juegos de Unitypara obtener todos los consejos y las mejores prácticas.