No hace mucho tiempo, apareció un excelente artículo sobre Habré Optimization de recolección de basura en un servicio .NET altamente cargado . Este artículo es muy interesante porque los autores, armados con la teoría, hicieron lo imposible antes: optimizaron su aplicación utilizando el conocimiento del GC. Y si antes no teníamos idea de cómo funciona este GC, ahora se nos presenta en bandeja de plata gracias a los esfuerzos de Konrad Cocos en su libro Pro .NET Memory Management . ¿Qué conclusiones he sacado yo mismo? Hagamos una lista de áreas problemáticas y pensemos cómo resolverlas.
En el reciente taller CLRium # 5: Garbage Collector, hablamos sobre GC todo el día. Sin embargo, decidí publicar un informe con decodificación de texto. Esta es una charla sobre conclusiones con respecto a la optimización de aplicaciones.
Reduce la conectividad entre generaciones
El problema
Para optimizar la velocidad de recolección de basura, el GC recolecta la generación más joven siempre que sea posible. Pero para hacer esto, también necesita información sobre enlaces de generaciones anteriores (en este caso, actúan como una raíz adicional): de la tabla de tarjetas.
Al mismo tiempo, un enlace de la generación más vieja a la más joven te obliga a cubrir el área con una tabla de cartas:
- Superposición de 4 bytes 4 kb o máx. 320 objetos - para arquitectura x86
- 8 bytes superpuestos 8 kb o máx. 320 objetos - para arquitectura x64
Es decir GC, al verificar la tabla de tarjetas, al encontrar un valor distinto de cero en ella, se ve obligado a verificar un máximo de 320 objetos para detectar la presencia de enlaces salientes en nuestra generación.
Por lo tanto, los enlaces dispersos en la generación más joven harán que GC requiera más tiempo
Solución
- Localice objetos con conexiones en la generación más joven, cercana;
- Si se supone el tráfico de objetos de generación cero, use tirar. Es decir hacer un grupo de objetos (no habrá nuevos: no habrá objetos de generación cero). Y además, al "calentar" el grupo con dos GC consecutivos de modo que se garantice que su contenido fallará en la segunda generación, evitará los enlaces a la generación más joven y tendrá ceros en la tabla de cartas;
- Evite los vínculos con la generación más joven;
Evite la conectividad fuerte
El problema
Como se desprende de los algoritmos de la fase de compresión de objetos en SOH:
- Para comprimir el montón, debe rodear el árbol y verificar todos los enlaces, corrigiéndolos para nuevos valores
- Además, los enlaces de la tabla de cartas afectan a grupos enteros de objetos.
Por lo tanto, la fuerte conectividad general de los objetos puede conducir a un hundimiento durante la GC.
Solución
- Tener objetos fuertemente conectados cerca, en una generación
- Evite enlaces innecesarios en general (por ejemplo, en lugar de duplicar esto-> manejar enlaces, use el ya existente this-> Service-> handle)
- Evite el código con conectividad oculta. Por ejemplo, cierres
Monitorear el uso del segmento
El problema
Durante el trabajo intensivo, puede surgir una situación en la que la asignación de nuevos objetos genera demoras: la asignación de nuevos segmentos bajo el montón y su posterior descompresión al limpiar la basura
Solución
- Uso de las utilidades PerfMon / Sysinternal para controlar los puntos de selección de nuevos segmentos y su liberación y liberación
- Si estamos hablando de LOH, que es un tráfico denso de búfer, use ArrayPool
- Si hablamos de SOH, asegúrese de que los objetos de la misma vida estén resaltados cerca, proporcionando Barrido en lugar de Recolección
- SOH: usar agrupaciones de objetos
No asigne memoria en secciones cargadas de código
El problema
La sección cargada de código asigna memoria:
- Como resultado, GC selecciona una ventana de asignación no de 1Kb, sino de 8Kb.
- Si la ventana se queda sin espacio, esto conduce a un GC y a la expansión de la zona cerrada
- Un flujo denso de nuevos objetos hará que los objetos de corta duración de otros hilos pasen rápidamente a la generación anterior con peores condiciones de recolección de basura.
- Lo que aumentará el tiempo de recolección de basura.
- Lo que conducirá a detener el mundo por más tiempo incluso en modo concurrente
Solución
- Prohibición total del uso de cierres en secciones críticas del código.
- Prohibición completa del boxeo en secciones críticas del código (puede usar la emulación tirando si es necesario)
- Cuando sea necesario crear un objeto temporal para el almacenamiento de datos, use estructuras. Mejor es la estructura de ref. Cuando el número de campos es más de 2, transmita por ref.
Evite asignaciones de memoria innecesarias en LOH
El problema
Colocar matrices en LOH conduce a la fragmentación o ponderación del procedimiento GC
Solución
- Use la división de matrices en submatrices y una clase que encapsule la lógica de trabajar con tales matrices (es decir, en lugar de List <T>, donde se almacena la mega-matriz, su MyList con matriz [] [], dividiendo la matriz un poco más corta)
- Las matrices irán a SOH
- Después de un par de recolecciones de basura, se acostarán al lado de objetos siempre vivos y dejarán de influir en la recolección de basura.
- Controle el uso de matrices dobles con una longitud de más de 1000 elementos.
Donde esté justificado y sea posible, use la pila de hilos
El problema
Hay varios objetos ultracortos u objetos que viven dentro de una llamada a un método (incluidas las llamadas internas). Crean tráfico de objetos.
Solución
- Usando la asignación de memoria en la pila donde sea posible:
- No carga mucho
- No carga GC
- Liberación de memoria: instantánea
- Utilice
Span T x = stackalloc T[];
en lugar de la new T[]
donde sea posible - Use
Span/Memory
cuando sea posible - Convierta algoritmos a tipos de
ref stack
(StackList: struct, ValueStringBuilder )
Objetos libres lo antes posible
El problema
Concebidos como de corta duración, los objetos caen en gen1 y, a veces, en gen2.
Esto da como resultado un GC más pesado que dura más
Solución
- Debe liberar la referencia del objeto lo antes posible
- Si un algoritmo extenso contiene código que funciona con cualquier objeto, está espaciado por código. Pero que se puede agrupar en un solo lugar, es necesario agruparlo, lo que permite que se recopilen antes.
- Por ejemplo, en la línea 10, se retiró la colección, y en la línea 120, se filtró.
No es necesario llamar a GC.Collect ()
El problema
A menudo parece que si llama a GC.Collect (), solucionará la situación
Solución
- Es mucho más correcto aprender los algoritmos de operación del GC, ver la aplicación bajo ETW y otras herramientas de diagnóstico (JetBrains dotMemory, ...)
- Optimizar las áreas más problemáticas
Evita fijar
El problema
Fijar plantea una serie de problemas:
- Complica la recolección de basura
- Crea espacios de memoria libres (elementos de lista libre de nodos, tabla de ladrillos, cubos)
- Puede dejar algunos objetos en la generación más joven, mientras se forman enlaces desde la mesa de juego.
Solución
Si no hay otra salida, use fixed () {}. Este método de confirmación no realiza una confirmación real: solo ocurre cuando el GC ha funcionado dentro de llaves.
Evitar finalización
El problema
La finalización no se llama determinísticamente:
- Dispose no invitado () resulta en finalización con todos los enlaces salientes del objeto
- Los objetos dependientes se retrasan más de lo previsto
- Envejecer, mudarse a las generaciones mayores
- Si al mismo tiempo contienen enlaces a los más jóvenes, generan enlaces desde la tabla de cartas
- Complicando el ensamblaje de generaciones anteriores, fragmentándolas y conduciendo a la compactación en lugar del barrido
Solución
Llama suavemente a Dispose ()
Evita demasiados hilos
El problema
Con una gran cantidad de subprocesos, el contexto de asignación crece a medida que se asignan a cada hilo:
- Como resultado, GC.Collect llega más rápido.
- Debido a la falta de espacio en el segmento efímero, Collect seguirá Collective Sweep
Solución
- Controla la cantidad de hilos por la cantidad de núcleos
Evite el tráfico de objetos de diferentes tamaños.
El problema
Al traficar objetos de diferentes tamaños y vidas, se produce fragmentación:
- Aumentar la proporción de fragmentación
- Activación de recopilación con una fase de cambio de dirección en todos los objetos de referencia
Solución
Si se supone el tráfico de objetos:
- Verifique la presencia de campos adicionales, aproximándose al tamaño
- Verifique la falta de manipulación de cadenas: cuando sea posible, reemplace con ReadOnlySpan / ReadOnlyMemory
- Libere el enlace lo antes posible
- Aprovecha el tirón
- Cálidos cachés y piscinas con un doble GC para compactar objetos. Por lo tanto, evita problemas con la tabla de cartas.