Compartir por


Mejora del rendimiento de la recolección de basura en aplicaciones WinUI

Las aplicaciones WinUI creadas con Windows App SDK y escritas en C# obtienen la administración automática de memoria del recolector de elementos no utilizados de .NET. En este artículo se resumen los procedimientos recomendados de comportamiento y rendimiento para el recolector de elementos no utilizados de .NET en aplicaciones winUI. Para obtener más información sobre cómo funciona el recolector de elementos no utilizados de .NET y las herramientas para depurar y analizar el rendimiento del recolector de elementos no utilizados, consulte Recolección de elementos no utilizados.

Nota:

La necesidad de intervenir en el comportamiento predeterminado del recolector de elementos no utilizados es muy indicativo de problemas generales de memoria con la aplicación. Use la herramienta Uso de memoria en Visual Studio y las instrucciones de recolección de elementos no utilizados y rendimiento para identificar los objetos que sobreviven a las recopilaciones.

El recolector de elementos no utilizados determina cuándo se debe ejecutar equilibrando el consumo de memoria del montón administrado con la cantidad de trabajo que debe hacer una recolección de elementos no utilizados. Una de las formas en que lo hace el recolector de basura es dividiendo el montón en generaciones y recolectando solo parte del montón la mayoría del tiempo. Hay tres generaciones en el montón administrado:

  • Generación 0. Esta generación contiene objetos recién asignados a menos que tengan 85 KB o más, en cuyo caso forman parte del montón de objetos grandes. El montón de objetos grandes se recoge en las colecciones de la generación 2. Las colecciones de generación 0 son el tipo de colección más frecuente y limpian objetos de corta duración, como variables locales.
  • Generación 1. Esta generación contiene objetos que han sobrevivido a las colecciones de generación 0. Actúa como búfer entre la generación 0 y la generación 2. Las colecciones de generación 1 se producen con menos frecuencia que las colecciones de generación 0 y limpian los objetos temporales que estaban activos durante las colecciones de generación 0 anteriores. Una colección de generación 1 también recopila la generación 0.
  • Generación 2. Esta generación contiene objetos de larga duración que han sobrevivido a colecciones de generación 0 y generación 1. Las colecciones de generación 2 son las menos frecuentes y recopilan todo el montón administrado, incluido el montón de objetos grandes, que contiene objetos de 85 KB o más.

Puede medir el rendimiento del recolector de basura en dos aspectos: el tiempo que tarda en realizar la recolección y el consumo de memoria del montón administrado. Si tiene una aplicación pequeña con un tamaño de montón inferior a 100 MB, céntrese en reducir el consumo de memoria. Si tiene una aplicación con un heap administrado superior a 100 MB, céntrese solo en reducir el tiempo de recolección de basura. Aquí se muestra cómo puede ayudar al recolector de elementos no utilizados de .NET a lograr un mejor rendimiento.

Reducir el consumo de memoria

Liberar las referencias

Una referencia a un objeto de la aplicación impide que ese objeto y todos los objetos a los que hace referencia se recopilen. El compilador de .NET realiza un buen trabajo de detección cuando una variable ya no está en uso, por lo que los objetos mantenidos en esa variable serán aptos para la recopilación. Pero en algunos casos puede no ser obvio que algunos objetos tienen una referencia a otros objetos porque parte del grafo de objetos podría ser propiedad de las bibliotecas que usa la aplicación. Para obtener información sobre las herramientas y técnicas para averiguar qué objetos sobreviven a una recolección de basura, consulte Recolección de basura y rendimiento.

Activar una recolección de basura si es útil

Fuerce una recolección de basura solo después de haber medido el rendimiento de la aplicación y haya determinado que forzar una recolección mejorará su rendimiento.

Puede inducir una recolección de basura de una generación llamando a GC.Collect(n), donde n es la generación que desea recolectar (0, 1 o 2).

Nota:

Se recomienda no forzar una recolección de elementos no utilizados en la aplicación porque el recolector de elementos no utilizados usa muchas heurística para determinar el mejor momento para realizar una recolección y forzar una recolección es en muchos casos un uso innecesario de la CPU. Pero si sabe que tiene un gran número de objetos en la aplicación que ya no se usan y desea devolver esta memoria al sistema, puede ser adecuado forzar una recolección de elementos no utilizados. Por ejemplo, puedes inducir una colección al final de una secuencia de carga en un juego para liberar memoria antes de que se inicie el juego.   Para evitar inducir involuntariamente demasiadas recolecciones de basura, puede establecer GCCollectionMode en Optimizado. Esto indica al recolector de basura que comience una recolección solo si determina que la recolección sería lo suficientemente productiva como para justificarse.

Reducir el tiempo de recolección de basura

Esta sección se aplica si ha analizado tu aplicación y ha observado tiempos prolongados de recolección de basura. Los tiempos de pausa relacionados con la recolección de basura incluyen el tiempo necesario para ejecutar una sola pasada de recolección de basura y el tiempo total que la aplicación dedica a realizar dichas recolecciones. La cantidad de tiempo que se tarda en realizar una recopilación depende de la cantidad de datos activos que el recopilador tiene que analizar. La generación 0 y la generación 1 están limitadas en tamaño, pero la generación 2 sigue creciendo, ya que los objetos de larga duración están activos en la aplicación. Esto significa que los tiempos de recopilación de la generación 0 y la generación 1 están limitados, mientras que las colecciones de generación 2 pueden tardar más tiempo. La frecuencia con la que se ejecutan las recolecciones de elementos no utilizados depende principalmente de la cantidad de memoria que asigne, ya que una recolección de elementos no utilizados libera memoria para satisfacer las solicitudes de asignación.

El recolector de basura pausa ocasionalmente la aplicación para realizar sus tareas, pero no pausa necesariamente la aplicación durante toda la recolección. Normalmente, los tiempos de pausa no son perceptibles por el usuario en la aplicación, especialmente para colecciones de generación 0 y generación 1. La característica Recolección de elementos no utilizados en segundo plano del recolector de elementos no utilizados de .NET permite realizar recolecciones de generación 2 simultáneamente mientras se ejecuta la aplicación y solo pausará la aplicación durante breves períodos de tiempo. Pero no siempre es posible realizar una colección de generación 2 como colección en segundo plano. En ese caso, la pausa puede ser perceptible por el usuario si tiene un montículo lo suficientemente grande (más de 100 MB).

Las recolecciones frecuentes de elementos no utilizados pueden contribuir a aumentar la CPU, aumentar el consumo de energía, tiempos de carga más largos o reducir las velocidades de fotogramas en la aplicación. A continuación, se presentan algunas técnicas que puede utilizar para reducir el tiempo de recolección de basura y las pausas relacionadas con la recolección en su aplicación WinUI administrada.

Reducción de las asignaciones de memoria

Si no asigna ningún objeto, el recolector de basura no se ejecuta a menos que haya una condición de memoria baja en el sistema. Reducir la cantidad de memoria que asigna se traduce directamente en menos frecuentes recolecciones de basura.

Si en algunas secciones de la aplicación las pausas no son deseables, puedes asignar previamente los objetos necesarios durante un momento en que el rendimiento no sea tan crítico. Por ejemplo, un juego podría asignar todos los objetos necesarios para el juego durante la pantalla de carga de un nivel y no realizar asignaciones durante el juego. Esto evita pausas mientras el usuario está jugando al juego y puede dar lugar a una velocidad de fotogramas más alta y coherente.

Reducir las colecciones de generación 2 evitando objetos con una duración media

Las recolecciones de basura generacionales funcionan mejor cuando tienes objetos de vida muy corta y/o muy larga en tu aplicación. Los objetos de corta duración se recopilan en las colecciones más baratas de generación 0 y generación 1, y los objetos que son de larga duración se promueven a la generación 2, que se recopilan con poca frecuencia. Los objetos de larga duración son aquellos que están en uso durante toda la duración de la aplicación, o durante un período significativo de la aplicación, como durante una página o nivel de juego específico.

Si con frecuencia crea objetos que tienen una duración temporal pero que existen lo suficiente para ascender a la generación 2, entonces ocurren con más frecuencia las costosas colecciones de generación 2. Puede reducir las colecciones de generación 2 mediante el reciclaje de objetos existentes o la liberación de objetos más rápidamente.

Un ejemplo común de objetos con una duración a medio plazo son los que se usan para mostrar elementos en una lista por la que un usuario se desplaza. Si se crean objetos cuando los elementos de la lista se desplazan a la vista, y dejan de ser referenciados cuando los elementos de la lista se desplazan fuera de la vista, entonces su aplicación normalmente tiene un gran número de colecciones de la generación 2. En situaciones como esta, puede preasignar y reutilizar un conjunto de objetos para los datos que se muestran activamente al usuario y usar objetos de corta duración para cargar información a medida que los elementos de la lista aparecen.

Reducir las colecciones de generación 2 evitando objetos de gran tamaño con duraciones cortas

Cualquier objeto que sea de 85 KB o mayor se asigna en el montón de objetos grandes (LOH) y se recopila como parte de la generación 2. Si tiene variables temporales, como búferes, que son mayores de 85 KB, una colección de generación 2 las limpia. Limitar las variables temporales a menos de 85 KB reduce el número de colecciones de generación 2 en la aplicación. Una técnica común es crear un pool de búfer y reutilizar objetos del pool para evitar asignaciones temporales grandes.

Evitar objetos con muchas referencias

El recolector de basura determina qué objetos están vivos siguiendo las referencias entre objetos, empezando por las raíces de la aplicación. Para obtener más información, consulte ¿Qué ocurre durante una recolección de elementos no utilizados? Si un objeto contiene muchas referencias, hay más trabajo para el recolector de basura. Una técnica común, especialmente con objetos grandes, es convertir objetos con muchas referencias en objetos sin referencias. Por ejemplo, en lugar de almacenar una referencia, almacene un índice. Por supuesto, esta técnica solo funciona cuando es lógicamente posible hacerlo.

Reemplazar las referencias de objeto por índices puede ser un cambio complicado y disruptivo en la aplicación y es más eficaz para objetos grandes con un gran número de referencias. Haz esto solo si estás notando tiempos de recolección de basura grandes en tu aplicación relacionados con objetos con muchas referencias.