¿Cuándo es una buena idea forzar la recolección de basura?

135

Así que estaba leyendo una pregunta sobre cómo obligar al recolector de basura de C # a ejecutar donde casi todas las respuestas son iguales: puede hacerlo, pero no debe, a excepción de algunos casos muy raros . Lamentablemente, nadie allí explica cuáles son esos casos.

¿Me puede decir en qué tipo de escenario es realmente una idea buena o razonable forzar la recolección de basura?

No estoy pidiendo casos específicos de C #, sino todos los lenguajes de programación que tienen un recolector de basura. Sé que no puede forzar GC en todos los idiomas, como Java, pero supongamos que sí.

Omega
fuente
17
"sino más bien, todos los lenguajes de programación que tienen un recolector de basura" Los diferentes lenguajes (o, más adecuadamente, diferentes implementaciones ) usan diferentes métodos para la recolección de basura, por lo que es poco probable que encuentre una regla única para todos.
Coronel Treinta y dos
44
@Doval Si tiene una restricción de tiempo real y el GC no ofrece garantías de correspondencia, se encuentra entre una roca y un lugar difícil. Puede reducir las pausas no deseadas en lugar de no hacer nada, pero por lo que he escuchado es "más fácil" evitar la asignación en el curso normal de la operación.
3
Tenía la impresión de que si esperaba tener plazos estrictos en tiempo real, nunca usaría un lenguaje GC en primer lugar.
GregRos
44
No puedo ver cómo puede responder esto de una manera no específica de VM. Relevante para procesos de 32 bits, no relevante para procesos de 64 bits. .NET JVM y para el de gama alta
rwong
3
@DavidConrad puedes forzarlo en C #. De ahí la pregunta.
Omega

Respuestas:

127

Realmente no puede hacer declaraciones generales sobre la forma adecuada de utilizar todas las implementaciones de GC. Varían enormemente. Así que hablaré con el .NET al que te referiste originalmente.

Debe conocer el comportamiento del GC bastante íntimamente para hacerlo con lógica o razón.

El único consejo sobre la colección que puedo dar es: nunca lo hagas.

Si realmente conoce los intrincados detalles del GC, no necesitará mi consejo, por lo que no importará. Si no lo sabe ya con 100% de confianza que le ayudará, y tienen que buscar en línea y encontrar una respuesta como esta: Usted no debe estar llamando GC.Collect , o alternativamente: Usted debe ir a conocer los detalles de cómo funciona el GC por dentro y por fuera, y solo entonces sabrás la respuesta .

Hay un lugar seguro donde tiene sentido usar GC .

GC.Collect es una API disponible que puede usar para perfilar los tiempos de las cosas. Puede perfilar un algoritmo, recopilar y perfilar otro algoritmo inmediatamente después sabiendo que GC del primer algo no estaba ocurriendo durante el segundo sesgando los resultados.

Este tipo de perfil es la única vez que sugeriría recopilar manualmente a cualquiera.


Ejemplo contribuido de todos modos

Un posible caso de uso es que si carga cosas realmente grandes, terminarán en el Montón de objetos grandes que irá directamente a Gen 2, aunque nuevamente Gen 2 es para objetos de larga vida porque se acumula con menos frecuencia. Si sabe que está cargando objetos de corta duración en Gen 2 por cualquier motivo, puede borrarlos más rápidamente para mantener su Gen 2 más pequeño y sus colecciones más rápido.

Este es el mejor ejemplo que se me ocurre, y no es bueno: la presión de LOH que está generando aquí provocaría colecciones más frecuentes, y las colecciones son tan frecuentes como es posible, es probable que elimine el LOH de la misma manera que tan rápido como lo soplabas con objetos temporales. Simplemente no confío en mí mismo para presumir una mejor frecuencia de recolección que el GC en sí, sintonizado por personas mucho más inteligentes que yo.


Así que hablemos sobre algunas de las semánticas y mecanismos en .NET GC ... o ...

Todo lo que creo saber sobre el GC .NET

Por favor, cualquiera que encuentre errores aquí, corrígeme. Se sabe que gran parte de la GC es magia negra y, aunque traté de omitir detalles de los que no estaba seguro, probablemente todavía me equivoqué.

A continuación, faltan intencionalmente numerosos detalles de los que no estoy seguro, así como una gran cantidad de información que simplemente desconozco. Utilice esta información bajo su propio riesgo.


Conceptos de GC

El .NET GC ocurre en momentos inconsistentes, por eso se llama "no determinista", esto significa que no puede confiar en que ocurra en momentos específicos. También es un recolector de basura generacional, lo que significa que divide sus objetos en la cantidad de GC que han pasado.

Los objetos en el montón Gen 0 han vivido a través de 0 colecciones, estas se han hecho recientemente, por lo que no se ha producido ninguna colección desde su creación. Los objetos en su montón Gen 1 han vivido a través de un pase de colección, y del mismo modo los objetos en su montón Gen 2 han vivido a través de 2 pases de colección.

Ahora vale la pena señalar la razón por la que califica estas generaciones y particiones específicas en consecuencia. .NET GC solo reconoce estas tres generaciones, porque los pases de recolección que pasan por estos tres montones son todos ligeramente diferentes. Algunos objetos pueden sobrevivir a la recolección de miles de veces. El GC simplemente los deja al otro lado de la partición de almacenamiento dinámico Gen 2, no tiene sentido dividirlos en otro lugar porque en realidad son Gen 44; el pase de colección sobre ellos es el mismo que todo en el montón de Gen 2.

Hay propósitos semánticos para estas generaciones específicas, así como mecanismos implementados que los honran, y los abordaré en un momento.


¿Qué hay en una colección?

El concepto básico de un pase de colección GC es que verifica cada objeto en un espacio de almacenamiento dinámico para ver si todavía hay referencias vivas (raíces GC) a estos objetos. Si se encuentra una raíz GC para un objeto, significa que el código que se está ejecutando actualmente puede alcanzar y usar ese objeto, por lo que no se puede eliminar. Sin embargo, si no se encuentra una raíz GC para un objeto, significa que el proceso en ejecución ya no necesita el objeto, por lo que puede eliminarlo para liberar memoria para nuevos objetos.

Ahora, una vez que haya terminado de limpiar un montón de objetos y dejar algunos en paz, habrá un efecto secundario desafortunado: espacios libres entre los objetos vivos donde se eliminaron los muertos. Esta fragmentación de la memoria, si se deja sola, simplemente desperdiciaría la memoria, por lo que las colecciones normalmente harán lo que se llama "compactación", donde toman todos los objetos vivos que quedan y los aprietan en el montón para que la memoria libre sea contigua en un lado del montón para Gen 0.

Ahora, dada la idea de 3 montones de memoria, todos particionados por el número de pases de colección que han vivido, hablemos de por qué existen estas particiones.


Colección Gen 0

Siendo Gen 0 los objetos más nuevos, tiende a ser muy pequeño, por lo que puede recolectarlo con seguridad con mucha frecuencia . La frecuencia garantiza que el montón se mantenga pequeño y las colecciones sean muy rápidas porque se están acumulando en un montón tan pequeño. Esto se basa más o menos en una heurística que afirma: una gran mayoría de los objetos temporales que crea son muy temporales, por lo que ya no se usarán ni se referenciarán casi inmediatamente después del uso, por lo que se pueden recolectar.


Colección Gen 1

Gen 1, siendo objetos que no cayeron en esta categoría muy temporal de objetos, aún puede ser de corta duración, porque aún así, una gran parte de los objetos creados no se usan por mucho tiempo. Por lo tanto, Gen 1 también recolecta con bastante frecuencia, nuevamente manteniendo su montón pequeño para que sus colecciones sean rápidas. Sin embargo, la suposición es menos de TI de objetos que son temporales Gen 0, por lo que se acumula con menos frecuencia que Gen 0

Diré francamente que no conozco los mecanismos técnicos que difieren entre el pase de recolección de Gen 0 y el Gen 1, si hay alguno que no sea la frecuencia que recolectan.


Colección Gen 2

Gen 2 ahora debe ser la madre de todos los montones, ¿verdad? Bueno, sí, eso es más o menos correcto. Es donde viven todos sus objetos permanentes: el objeto en el que Main()vive, por ejemplo, y todo lo que hace Main()referencia porque se arraigará hasta que Main()regrese al final de su proceso.

Dado que Gen 2 es un cubo para básicamente todo lo que las otras generaciones no pudieron recolectar, sus objetos son en gran medida permanentes, o al menos de larga duración. Por lo tanto, reconocer muy poco de lo que hay en Gen 2 en realidad será algo que se puede recopilar, no es necesario que se recopile con frecuencia. Esto permite que su colección también sea más lenta, ya que se ejecuta con mucha menos frecuencia. Entonces, esto es básicamente donde han agregado todos los comportamientos adicionales para escenarios extraños, porque tienen tiempo para ejecutarlos.


Montón de objetos grandes

Un ejemplo de los comportamientos adicionales de Gen 2 es que también realiza la recopilación en el Montón de objetos grandes. Hasta ahora he estado hablando completamente sobre el montón de objetos pequeños, pero el tiempo de ejecución de .NET asigna cosas de ciertos tamaños a un montón separado debido a lo que denominé compactación anterior. La compactación requiere mover objetos cuando las colecciones terminan en el montón de objetos pequeños. Si hay un objeto vivo de 10 MB en Gen 1, le llevará mucho más tiempo completar la compactación después de la colección, lo que ralentizará la colección de Gen 1. De modo que ese objeto de 10mb se asigna al Montón de objetos grandes y se recopila durante Gen 2, que se ejecuta con poca frecuencia.


Finalizacion

Otro ejemplo son los objetos con finalizadores. Coloca un finalizador en un objeto que hace referencia a recursos más allá del alcance de .NETs GC (recursos no administrados). El finalizador es la única forma en que el GC puede exigir que se recopile un recurso no administrado: usted implementa su finalizador para realizar la recolección / eliminación / liberación manual del recurso no administrado para garantizar que no se filtre en su proceso. Cuando el GC llega a ejecutar su finalizador de objetos, su implementación borrará el recurso no administrado, haciendo que el GC sea capaz de eliminar su objeto sin correr el riesgo de una fuga de recursos.

El mecanismo con el que los finalizadores hacen esto es mediante referencia directa en una cola de finalización. Cuando el tiempo de ejecución asigna un objeto con un finalizador, agrega un puntero a ese objeto a la cola de finalización y bloquea su objeto en su lugar (llamado fijación) para que la compactación no lo mueva, lo que rompería la referencia de la cola de finalización. A medida que ocurren los pases de recopilación, eventualmente se descubrirá que su objeto ya no tiene una raíz GC, pero la finalización debe ejecutarse antes de que pueda recopilarse. Por lo tanto, cuando el objeto está muerto, la colección moverá su referencia de la cola de finalización y colocará una referencia en lo que se conoce como la cola "FReachable". Luego la colección continúa. En otro momento "no determinista" en el futuro, un hilo separado conocido como el hilo Finalizador pasará por la cola FReachable, ejecutando los finalizadores para cada uno de los objetos referenciados. Una vez finalizado, la cola FReachable está vacía y se ha volteado un poco en el encabezado de cada objeto que dice que no necesitan finalización (este bit también se puede voltear manualmente conGC.SuppressFinalizeque es común en los Dispose()métodos), también sospecho que ha desanclado los objetos, pero no me cite al respecto. La próxima colección que aparezca en cualquier montón en el que se encuentre este objeto, finalmente la recopilará. Las colecciones Gen 0 ni siquiera prestan atención a los objetos con ese bit de finalización necesario, los promueve automáticamente, sin siquiera verificar su raíz. Un objeto no enraizado que necesita finalización en Gen 1, será arrojado a la FReachablecola, pero la colección no hace nada más con él, por lo que vive en Gen 2. De esta manera, todos los objetos que tienen un finalizador, y no GC.SuppressFinalizeserá recolectado en Gen 2.

Jimmy Hoffa
fuente
44
@FlorianMargaine, sí ... decir algo sobre "GC" en todas las implementaciones realmente no tiene sentido ...
Jimmy Hoffa
10
tl; dr: use grupos de objetos en su lugar.
Robert Harvey
55
tl; dr: para cronometraje / perfilado, puede ser útil.
kutschkem
3
@Den después de leer mi descripción anterior de la mecánica (según tengo entendido), ¿cuál sería el beneficio tal como lo ves? ¿Limpia una gran cantidad de objetos en el SOH (o LOH?)? ¿Acabas de hacer que otros hilos pausen para esta colección? ¿Esa colección solo promocionó el doble de objetos en Gen 2 de lo que se eliminó? ¿La colección causó compactación en LOH (¿la tiene activada?)? ¿Cuántos montones de GC tiene y está su GC en modo servidor o escritorio? GC es un maldito iceberg, la traición está debajo de las aguas. Solo manténgase alejado. No soy lo suficientemente inteligente como para coleccionar cómodamente.
Jimmy Hoffa
44
@RobertHarvey Los grupos de objetos tampoco son una bala de plata. La generación 0 del recolector de basura ya es efectivamente un grupo de objetos: por lo general, su tamaño se ajusta al nivel de caché más pequeño y, por lo tanto, generalmente se crean nuevos objetos en la memoria que ya está en caché. Su grupo de objetos ahora está compitiendo contra el vivero del GC por el caché, y si la suma del vivero del GC y su grupo es mayor que el caché, obviamente va a tener errores de caché. Y si planea usar el paralelismo ahora, debe volver a implementar la sincronización y preocuparse por el intercambio falso.
Doval
68

Lamentablemente, nadie allí explica cuáles son esos casos.

Te daré algunos ejemplos. En general, es raro que forzar un GC sea una buena idea, pero puede valer la pena. Esta respuesta es de mi experiencia con literatura .NET y GC. Debe generalizarse bien a otras plataformas (al menos aquellas que tienen un GC significativo).

  • Puntos de referencia de diversos tipos. Desea un estado de almacenamiento dinámico administrado conocido cuando comienza un punto de referencia para que el GC no se active aleatoriamente durante los puntos de referencia. Cuando repite un punto de referencia, desea el mismo número y cantidad de trabajo de GC en cada repetición.
  • Liberación repentina de recursos. Por ejemplo, cerrar una ventana de GUI significativa o actualizar un caché (y liberar así los viejos contenidos de caché potencialmente grandes). El GC no puede detectar esto porque todo lo que está haciendo es establecer una referencia en nulo. El hecho de que este huérfano sea un gráfico de objeto completo no es fácilmente detectable.
  • Liberación de recursos no gestionados que se han filtrado . Esto nunca debería suceder, por supuesto, pero he visto casos en los que una biblioteca de terceros filtró cosas (como objetos COM). El desarrollador se vio obligado a inducir a veces una colección.
  • Aplicaciones interactivas como juegos . Durante los juegos tienen presupuestos de tiempo muy estrictos por cuadro (60Hz => 16 ms por cuadro). Para evitar problemas, necesita una estrategia para lidiar con los GC. Una de esas estrategias es retrasar los GC G2 tanto como sea posible y forzarlos en un momento oportuno, como una pantalla de carga o una escena de corte. El GC no puede saber cuándo es el mejor momento.
  • Control de latencia en general. Algunas aplicaciones web deshabilitan los GC y ejecutan periódicamente una colección G2 mientras se desconecta de la rotación del equilibrador de carga. De esa manera, la latencia G2 nunca se muestra al usuario.

Si su objetivo es el rendimiento, cuanto más raro sea el GC, mejor. En esos casos, forzar una colección no puede tener un impacto positivo (excepto por cuestiones más bien artificiales, como aumentar la utilización de la memoria caché de la CPU mediante la eliminación de objetos muertos intercalados en los activos). La colección por lotes es más eficiente para todos los coleccionistas que conozco. Para aplicaciones de producción en consumo de memoria en estado estable, inducir un GC no ayuda.

Los ejemplos dados anteriormente apuntan a la consistencia y la limitación del uso de la memoria. En esos casos, los GC inducidos pueden tener sentido.

Parece haber una idea generalizada de que el GC es una entidad divina que induce una colección siempre que sea realmente óptimo hacerlo. Ningún GC que conozco es tan sofisticado y, de hecho, es muy difícil ser óptimo para el GC. El GC sabe menos que el desarrollador. Sus heurísticas se basan en contadores de memoria y cosas como la tasa de recolección, etc. Las heurísticas suelen ser buenas, pero no capturan cambios repentinos en el comportamiento de la aplicación, como la liberación de grandes cantidades de memoria administrada. También es ciego a los recursos no administrados y a los requisitos de latencia.

Tenga en cuenta que los costos de GC varían con el tamaño del montón y el número de referencias en el montón. En un montón pequeño, el costo puede ser muy pequeño. He visto tasas de recopilación de G2 con .NET 4.5 de 1-2 GB / seg en una aplicación de producción con un tamaño de almacenamiento dinámico de 1 GB.

usr
fuente
Para el caso de control de latencia, supongo que en lugar de hacerlo periódicamente, también podría hacerlo por necesidad (es decir, cuando el uso de memoria crece por encima de un cierto umbral).
Paŭlo Ebermann
3
+1 para el penúltimo párrafo. Algunas personas tienen el mismo sentimiento sobre los compiladores y se apresuran a llamar a casi cualquier cosa "optimización prematura". Usualmente les digo algo similar.
Honza Brabec
2
+1 para ese párrafo también. Encuentro sorprendente que la gente piense que un programa de computadora escrito por otra persona necesariamente debe comprender las características de desempeño de su programa mejor que ellos mismos.
Mehrdad
1
@HonzaBrabec El problema es el mismo en ambos casos: si crees que sabes mejor que el GC o el compilador, entonces es muy fácil hacerte daño. Si realmente sabes más, entonces estás optimizando solo cuando sabes que no es prematuro.
svick
27

Como principio general, un recolector de basura se recolectará cuando se encuentre con "presión de memoria", y se considera una buena idea no hacerlo en otras ocasiones porque podría causar problemas de rendimiento o incluso pausas notables en la ejecución de su programa. Y, de hecho, el primer punto depende del segundo: para un recolector de basura generacional, al menos, se ejecuta de manera más eficiente cuanto mayor sea la proporción de basura a objetos buenos, por lo que para minimizar la cantidad de tiempo dedicado a pausar el programa , tiene que postergar y dejar que la basura se acumule tanto como sea posible.

El momento apropiado para invocar manualmente al recolector de basura, entonces, es cuando ha terminado de hacer algo que 1) es probable que haya creado mucha basura, y 2) el usuario espera que tome un tiempo y deje el sistema sin responder de todas formas. Un ejemplo clásico es al final de cargar algo grande (un documento, un modelo, un nuevo nivel, etc.)

Mason Wheeler
fuente
12

Una cosa que nadie ha mencionado es que, si bien el GC de Windows es increíblemente bueno, el GC en Xbox es basura (juego de palabras) .

Entonces, al codificar un juego XNA destinado a ejecutarse en XBox, es absolutamente crucial programar la recolección de basura en los momentos oportunos, o tendrá un horrible hipo intermitente de FPS. Además, en XBox es común usar structs way, mucho más a menudo de lo que lo haría normalmente, para minimizar la cantidad de objetos que se deben recolectar.

BlueRaja - Danny Pflughoeft
fuente
4

La recolección de basura es ante todo una herramienta de administración de memoria. Como tal, los recolectores de basura se recolectarán cuando haya presión de memoria.

Los recolectores de basura modernos son muy buenos y están mejorando, por lo que es poco probable que pueda mejorarlos recolectando manualmente. Incluso si puede mejorar las cosas hoy, puede ser que una mejora futura en el recolector de basura elegido haga que su optimización sea ineficaz o incluso contraproducente.

Sin embargo , los recolectores de basura generalmente no intentan optimizar el uso de recursos distintos de la memoria. En entornos de recolección de basura, la mayoría de los recursos valiosos que no son de memoria tienen un closemétodo o similar, pero hay algunas ocasiones en las que este no es el caso por alguna razón, como la compatibilidad con una API existente.

En estos casos, puede tener sentido invocar manualmente la recolección de basura cuando sabe que se está utilizando un recurso valioso que no es de memoria.

RMI

Un ejemplo concreto de esto es la invocación de método remoto de Java. RMI es una biblioteca de llamadas a procedimientos remotos. Por lo general, tiene un servidor, que pone a disposición de los clientes diversos objetos para su uso. Si un servidor sabe que ningún cliente está utilizando un objeto, entonces ese objeto es elegible para la recolección de basura.

Sin embargo, la única forma en que el servidor sabe esto es si el cliente se lo dice, y el cliente solo le dice al servidor que ya no necesita un objeto una vez que el cliente ha recolectado basura lo que sea que lo esté usando.

Esto presenta un problema, ya que el cliente puede tener mucha memoria libre, por lo que no puede ejecutar la recolección de basura con mucha frecuencia. Mientras tanto, el servidor puede tener muchos objetos no utilizados en la memoria, que no puede recopilar porque no sabe que el cliente no los está utilizando.

La solución en RMI es que el cliente ejecute la recolección de basura periódicamente, incluso cuando tiene mucha memoria libre, para garantizar que los objetos se recopilen rápidamente en el servidor.

James_pic
fuente
"En estos casos, puede tener sentido invocar manualmente la recolección de basura cuando sabe que se está utilizando un recurso valioso que no es de memoria" - si se está utilizando un recurso que no es de memoria, debería usar un usingbloque o llamar a un Closemétodo para asegúrese de que el recurso se descarte lo antes posible. Confiar en GC para limpiar recursos que no son de memoria no es confiable y causa todo tipo de problemas (particularmente con archivos que necesitan ser bloqueados para acceder, por lo que solo pueden abrirse una vez).
Julio
Y como se indica en la respuesta, cuando hay un closemétodo disponible (o el recurso se puede usar con un usingbloque), este es el enfoque correcto. La respuesta trata específicamente los casos raros en los que estos mecanismos no están disponibles.
James_pic
Mi opinión personal es que cualquier interfaz que administre un recurso que no sea de memoria pero que no proporcione un método cerrado es una interfaz que no debe usarse , porque no hay forma de usarla de manera confiable.
Jules
@Jules Estoy de acuerdo, pero a veces es inevitable. A veces las abstracciones se filtran, y usar una abstracción permeable es mejor que no usar abstracción. A veces necesita trabajar con código heredado que le exige hacer promesas que sabe que no puede cumplir. Sí, es raro, y debe evitarse si es posible, y hay una razón por la cual existen todas estas advertencias sobre forzar la recolección de basura, pero estas situaciones surgen y el OP preguntaba cómo son estas situaciones, lo que he respondido .
James_pic
2

La mejor práctica es no forzar una recolección de basura en la mayoría de los casos. (Todos los sistemas en los que he trabajado han forzado la recolección de basura, han subrayado problemas que, de haber sido resueltos, habrían eliminado la necesidad de forzar la recolección de basura y acelerado el sistema en gran medida).

Hay algunos casos en los que sabe más sobre el uso de la memoria que el recolector de basura. Es poco probable que esto sea cierto en una aplicación multiusuario o en un servicio que responde a más de una solicitud a la vez.

Sin embargo, en algunos tipos de procesamiento por lotes, usted sabe más que el GC. Por ejemplo, considere una aplicación que.

  • Se le da una lista de nombres de archivo en la línea de comando
  • Procesa un solo archivo y luego escribe el resultado en un archivo de resultados.
  • Mientras procesa el archivo, crea muchos objetos interconectados que no se pueden recopilar hasta que el procesamiento del archivo se haya completado (por ejemplo, un árbol de análisis)
  • No mantiene el estado de coincidencia entre los archivos que ha procesado .

Es posible que pueda hacer un caso (después de una cuidadosa) prueba de que debe forzar una recolección de basura completa después de haber procesado cada archivo.

Otro caso es un servicio que se activa cada pocos minutos para procesar algunos elementos y no mantiene ningún estado mientras está dormido . A continuación, obligando a una colección completa justo antes de irse a dormir puede valer la pena.

La única vez que consideraría forzar una colección es cuando sé que recientemente se ha creado una gran cantidad de objetos y que actualmente se hace referencia a muy pocos objetos.

Preferiría tener una API de recolección de basura cuando pudiera darle pistas sobre este tipo de cosas sin tener que forzar un GC por mí mismo.

Ver también " Tidbits de rendimiento de Rico Mariani "

Ian
fuente
2

Hay varios casos en los que es posible que desee llamar a gc () usted mismo.

  • [ Algunas personas dicen que esto no es bueno porque puede promover objetos a un espacio de generación anterior, lo que estoy de acuerdo no es algo bueno. Sin embargo, NO siempre es cierto que siempre habrá objetos que puedan promoverse. Ciertamente es posible que después de esta gc()llamada, muy pocos objetos permanezcan y mucho menos se trasladen al espacio de generación anterior ] Cuando va a crear una gran colección de objetos y utilizar mucha memoria. Simplemente desea limpiar tanto espacio como preparación como sea posible. Esto es solo sentido común. Al llamar gc()manualmente, no habrá verificación de gráfico de referencia redundante en parte de esa gran colección de objetos que está cargando en la memoria. En resumen, si ejecuta gc()antes de cargar mucho en la memoria, elgc() inducido durante la carga ocurre menos al menos una vez cuando la carga comienza a crear presión de memoria.
  • Cuando haya terminado de cargar una gran colección degrandeobjetos y es poco probable que cargue más objetos en la memoria. En resumen, pasas de la fase de creación a la fase de uso. Al llamar gc()dependiendo de la implementación, la memoria utilizada será compactada, lo que mejora enormemente la localidad de caché. Esto dará como resultado una mejora masiva en el rendimiento que no obtendrá de la creación de perfiles .
  • Similar al primero, pero desde el punto de vista de que si lo hace gc()y la implementación de administración de memoria es compatible, creará una continuidad mucho mejor para su memoria física. Esto nuevamente hace que la nueva gran colección de objetos sea más continua y compacta, lo que a su vez mejora el rendimiento
InformadoA
fuente
1
¿Alguien puede señalar la razón del voto negativo? Yo mismo no sé lo suficiente para juzgar la respuesta (a primera vista, tiene sentido para mí).
Omega
1
Supongo que tienes un voto negativo para el tercer punto. Potencialmente también por decir "Esto es solo sentido común".
immibis
2
Cuando crea una gran colección de objetos, el GC debe ser lo suficientemente inteligente como para saber si se necesita una colección. Lo mismo cuando la memoria necesita ser compactada. Confiar en el GC para optimizar la localidad de memoria de objetos relacionados no parece confiable. Creo que puedes encontrar otras soluciones (estructura, inseguro, ...). (No soy el votante).
Guillaume
3
Su primera idea de un buen momento es solo un mal consejo en mi opinión. Hay muchas posibilidades de que haya habido una colección recientemente, por lo que su intento de recolectar nuevamente simplemente promoverá arbitrariamente objetos para las generaciones posteriores, lo que casi siempre es malo. Las generaciones posteriores tienen colecciones que tardan más en comenzar, aumentando sus tamaños de almacenamiento dinámico "para despejar la mayor cantidad de espacio posible" solo hace que esto sea más problemático. Además, si está a punto de aumentar la presión de la memoria con una carga, es probable que comience a inducir colecciones de todos modos, lo que se ejecutará más lentamente porque aumentó Gen1 / 2
Jimmy Hoffa
2
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.Si asigna una tonelada de objetos seguidos, es probable que ya estén compactados. En todo caso, la recolección de basura puede barajarlos un poco. De cualquier manera, usar estructuras de datos densas y no saltar al azar en la memoria tendrá un mayor impacto. Si está utilizando una ingenua lista vinculada de un elemento por nodo, ninguna cantidad de trucos manuales de GC lo compensará.
Doval
2

Un ejemplo del mundo real:

Tenía una aplicación web que utilizaba un conjunto de datos muy grande que rara vez cambiaba y al que debía accederse muy rápidamente (lo suficientemente rápido para la respuesta por pulsación de tecla a través de AJAX).

Lo bastante obvio aquí es cargar el gráfico relevante en la memoria y acceder a él desde allí en lugar de a la base de datos, actualizando el gráfico cuando cambia la base de datos.

Pero al ser muy grande, una carga ingenua habría ocupado al menos 6 GB de memoria con los datos debido al crecimiento en el futuro. (No tengo cifras exactas, una vez que estaba claro que mi máquina de 2 GB estaba tratando de hacer frente a al menos 6 GB, tenía todas las medidas que necesitaba para saber que no iba a funcionar).

Afortunadamente, había una gran cantidad de objetos inmutables en paletas en este conjunto de datos que eran iguales entre sí; Una vez que me di cuenta de que cierto lote era igual a otro lote, pude alias una referencia a la otra permitiendo que se recopilaran muchos datos y, por lo tanto, encajar todo en menos de medio concierto.

Todo bien, pero para esto todavía se agita a través de más de 6 GB de objetos en el espacio de aproximadamente medio minuto para llegar a este estado. Dejado solo, GC no hizo frente; el aumento en la actividad sobre el patrón habitual de la aplicación (mucho menos pesado en desasignaciones por segundo) fue demasiado agudo.

Entonces, llamar periódicamente GC.Collect()durante este proceso de construcción significaba que todo funcionaba sin problemas. Por supuesto, no llamé manualmente GC.Collect()el resto del tiempo que se ejecuta la aplicación.

Este caso del mundo real es un buen ejemplo de las pautas de cuándo debemos usar GC.Collect():

  1. Úselo con un caso relativamente raro de muchos objetos disponibles para la colección (se pusieron a disposición megabytes, y esta creación de gráficos fue un caso muy raro durante la vida útil de la aplicación (aproximadamente un minuto por semana).
  2. Hágalo cuando una pérdida de rendimiento sea relativamente tolerable; esto sucedió solo en el inicio de la aplicación. (Otro buen ejemplo de esta regla es entre niveles durante un juego u otros puntos en un juego donde los jugadores no se molestarán por una pausa).
  3. Perfil para estar seguro de que realmente hay una mejora. (Bastante fácil; "Funciona" casi siempre es mejor que "no funciona").

La mayoría de las veces, cuando pensé que podría tener un caso en el GC.Collect()que valía la pena llamar, porque los puntos 1 y 2 aplicados, el punto 3 sugirió que empeoró las cosas o al menos no mejoró las cosas (y con poca o ninguna mejora, inclinarse hacia no llamar por sobre llamada ya que el enfoque tiene más probabilidades de resultar mejor durante la vida útil de una aplicación).

Jon Hanna
fuente
0

Tengo un uso para la eliminación de basura que es algo poco ortodoxo.

Existe esta práctica equivocada que, lamentablemente, es muy frecuente en el mundo de C #, de implementar la eliminación de objetos utilizando el lenguaje feo, torpe, poco elegante y propenso a errores conocido como disposición desechable ID . MSDN lo describe en detalle , y muchas personas lo juran, lo siguen religiosamente, pasan horas y horas discutiendo con precisión cómo se debe hacer, etc.

(Tenga en cuenta que lo que estoy llamando feo aquí no es el patrón de eliminación de objetos en sí; lo que estoy llamando feo es el IDisposable.Dispose( bool disposing )idioma particular ).

Este idioma fue inventado porque supuestamente es imposible garantizar que el recolector de basura siempre invoque el destructor de sus objetos para limpiar los recursos, por lo que las personas realizan la limpieza de los recursos dentro IDisposable.Dispose(), y en caso de que lo olviden, también lo intentan una vez más. dentro del destructor. Ya sabes, por si acaso.

Pero entonces es IDisposable.Dispose()posible que tenga objetos administrados y no administrados para limpiar, pero los administrados no se pueden limpiar cuando IDisposable.Dispose()se invoca desde dentro del destructor, porque el recolector de basura ya los ha cuidado en ese momento, por lo que Es esta la necesidad de un Dispose()método separado que acepte un bool disposingindicador para saber si los objetos administrados y no administrados deben limpiarse, o solo los no administrados.

Disculpe, pero esto es una locura.

Voy por el axioma de Einstein, que dice que las cosas deberían ser lo más simples posible, pero no más simples. Claramente, no podemos omitir la limpieza de recursos, por lo que la solución más simple posible debe incluir al menos eso. La siguiente solución más simple consiste en disponer siempre de todo en el momento preciso en que se supone que se debe eliminar, sin complicar las cosas al confiar en el destructor como alternativa.

Ahora, estrictamente hablando, es imposible garantizar que ningún programador cometerá el error de olvidar invocar IDisposable.Dispose(), pero lo que podemos hacer es usar el destructor para detectar este error. Es muy simple, en realidad: todo lo que el destructor tiene que hacer es generar una entrada de registro si detecta que el disposedindicador del objeto desechable nunca se configuró true. Por lo tanto, el uso del destructor no es una parte integral de nuestra estrategia de eliminación, pero es nuestro mecanismo de garantía de calidad. Y dado que esta es una prueba de solo modo de depuración, podemos colocar todo nuestro destructor dentro de un #if DEBUGbloque, por lo que nunca incurrirá en ninguna penalización de destrucción en un entorno de producción. (El IDisposable.Dispose( bool disposing )idioma prescribe queGC.SuppressFinalize() debe invocarse con precisión para disminuir la sobrecarga de finalización, pero con mi mecanismo es posible evitar por completo esa sobrecarga en el entorno de producción).

Lo que se reduce a un argumento eterno de error duro versus error suave : el IDisposable.Dispose( bool disposing )modismo es un enfoque de error suave y representa un intento de permitir que el programador olvide invocar Dispose()sin que el sistema falle, si es posible. El enfoque de error duro dice que el programador siempre debe asegurarse de que Dispose()se invoque. La penalización generalmente prescrita por el enfoque de error duro en la mayoría de los casos es el fracaso de la aserción, pero para este caso particular hacemos una excepción y disminuimos la penalización a una simple emisión de una entrada de registro de error.

Por lo tanto, para que este mecanismo funcione, la versión DEBUG de nuestra aplicación debe realizar una eliminación completa de basura antes de salir, para garantizar que se invoquen todos los destructores y, por lo tanto, atrape los IDisposableobjetos que olvidamos eliminar.

Mike Nakis
fuente
Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()En realidad, no lo es, aunque no creo que C # sea capaz de hacerlo. No exponga el recurso; en su lugar, proporcione un DSL para describir todo lo que hará con él (básicamente, una mónada), más una función que adquiere el recurso, hace las cosas, lo libera y devuelve el resultado. El truco consiste en utilizar el sistema de tipos para garantizar que si alguien introduce de contrabando una referencia al recurso, no se puede utilizar en otra llamada a la función de ejecución.
Doval
2
El problema con Dispose(bool disposing)(que no está definido IDisposablees que se usa para tratar la limpieza de los objetos administrados y no administrados que el objeto tiene como un campo (o de los que es responsable), es resolver el problema incorrecto. objetos no administrados en un objeto administrado sin tener que preocuparse por otros objetos desechables, entonces todos los Dispose()métodos serán uno de esos (haga que el finalizador haga la misma limpieza si es necesario) o solo tenga objetos administrados para desechar (no tenga un finalizador en absoluto), y la necesidad de bool disposingque desaparezca.
Jon Hanna
-1 mal consejo debido a cómo funciona realmente la finalización. Estoy totalmente de acuerdo con su punto de que el dispose(disposing)idioma es terribad, pero lo digo porque las personas a menudo usan esa técnica y finalizadores cuando solo tienen recursos administrados (el DbConnectionobjeto, por ejemplo, es administrado , no está pinchado ni ordenado), Y USTED DEBE SOLAMENTE ALGUNA VEZ IMPLEMENTE UN FINALIZADOR CON CÓDIGO NO MANEJADO, PINVOKADO, COM MARSHALLED O INSEGURO . En mi respuesta detallé anteriormente cuán lamentablemente costosos son los finalizadores, no los use a menos que tenga recursos no administrados en su clase.
Jimmy Hoffa
2
Casi quiero darte +1 solo porque estás denunciando algo que mucha gente toma como algo importante en el dispose(dispoing)idioma, pero la verdad es que es tan frecuente porque la gente tiene tanto miedo de las cosas de GC que algo tan poco relacionado como eso ( disposedebería tener mucho que ver con GC) les merece tomar el medicamento recetado sin siquiera investigarlo. Bien por inspeccionarlo, pero te perdiste el conjunto más grande (alienta a los finalizadores más a menudo de lo que deberían ser)
Jimmy Hoffa
1
@JimmyHoffa gracias por tu aporte. Estoy de acuerdo en que un finalizador normalmente solo debe usarse para liberar recursos no administrados, pero ¿no estaría de acuerdo con que en la compilación DEBUG esta regla no es aplicable, y que en la compilación DEBUG deberíamos ser libres de usar finalizadores para detectar errores? Eso es todo lo que estoy sugiriendo aquí, así que no veo por qué te molestas. Consulte también programmers.stackexchange.com/questions/288715/… para obtener una explicación más extensa de este enfoque en el lado de Java del mundo.
Mike Nakis
0

¿Me puede decir en qué tipo de escenario es realmente una idea buena o razonable forzar la recolección de basura? No estoy pidiendo casos específicos de C #, sino todos los lenguajes de programación que tienen un recolector de basura. Sé que no puede forzar GC en todos los idiomas, como Java, pero supongamos que sí.

Hablando muy teóricamente y sin tener en cuenta problemas como algunas implementaciones de GC que ralentizan las cosas durante sus ciclos de recolección, el escenario más grande que se me ocurre para forzar la recolección de basura es un software de misión crítica donde las fugas lógicas son preferibles a colgar los punteros, por ejemplo, porque se bloquea en momentos inesperados puede costar vidas humanas o algo por el estilo.

Si nos fijamos en algunos de los juegos indies de mala calidad escritos con lenguajes GC como los juegos Flash, se filtran como locos pero no se bloquean. Pueden tomar diez veces la memoria 20 minutos para jugar porque una parte de la base de código del juego olvidó establecer una referencia a nula o eliminarla de una lista, y las velocidades de fotogramas pueden comenzar a sufrir, pero el juego aún funciona. Un juego similar escrito con codificación de mala calidad C o C ++ podría fallar como resultado de acceder a punteros colgantes como resultado del mismo tipo de error de gestión de recursos, pero no se filtraría tanto.

Para los juegos, el bloqueo puede ser preferible en el sentido de que puede detectarse y repararse rápidamente, pero para un programa de misión crítica, estrellarse en momentos totalmente inesperados podría matar a alguien. Por lo tanto, los casos principales que creo serían escenarios en los que no fallar o que otras formas son de seguridad son absolutamente críticos, y una filtración lógica es algo relativamente trivial en comparación.

El escenario principal donde creo que es malo forzar GC es para cosas donde la fuga lógica es en realidad menos preferible que un bloqueo. Con los juegos, por ejemplo, el bloqueo no necesariamente matará a nadie y podría detectarse y repararse fácilmente durante las pruebas internas, mientras que una fuga lógica podría pasar desapercibida incluso después de que se envíe el producto, a menos que sea tan grave que haga que el juego no se pueda jugar en minutos . En algunos dominios, un bloqueo fácilmente reproducible que ocurre en las pruebas a veces es preferible a una fuga que nadie nota de inmediato.

Otro caso en el que puedo pensar que sería preferible forzar GC en un equipo es para un programa de muy corta duración, como algo ejecutado desde la línea de comando que realiza una tarea y luego se apaga. En ese caso, la vida útil del programa es demasiado corta para que cualquier tipo de fuga lógica no sea trivial. Las fugas lógicas, incluso para grandes recursos, generalmente solo se vuelven problemáticas horas o minutos después de ejecutar el software, por lo que es improbable que un software que solo se ejecute durante 3 segundos tenga problemas con fugas lógicas, y podría hacer mucho Es más sencillo escribir programas de corta duración si el equipo acaba de usar GC.


fuente