¿Por qué Large Object Heap y por qué nos importa?

105

He leído sobre Generaciones y montón de objetos grandes. Pero todavía no entiendo cuál es el significado (o beneficio) de tener un montón de objetos grandes.

¿Qué podría haber salido mal (en términos de rendimiento o memoria) si CLR solo hubiera confiado en la Generación 2 (considerando que el umbral para Gen0 y Gen1 es pequeño para manejar objetos grandes) para almacenar objetos grandes?

Manish Basantani
fuente
6
Esto me da dos preguntas para los diseñadores de .NET: 1. ¿Por qué no se llama a una desfragmentación LOH antes de lanzar una OutOfMemoryException? 2. ¿Por qué no hacer que los objetos LOH tengan afinidad por permanecer juntos (los grandes prefieren el final del montón y los pequeños al principio)
Jacob Brewer

Respuestas:

195

Una recolección de basura no solo se deshace de los objetos no referenciados, sino que también compacta el montón. Esa es una optimización muy importante. No solo hace que el uso de la memoria sea más eficiente (sin agujeros sin usar), sino que hace que la memoria caché de la CPU sea mucho más eficiente. El caché es un gran problema en los procesadores modernos, son un orden de magnitud más rápido que el bus de memoria.

La compactación se realiza simplemente copiando bytes. Sin embargo, eso lleva tiempo. Cuanto más grande sea el objeto, es más probable que el costo de copiarlo supere las posibles mejoras en el uso de la memoria caché de la CPU.

Así que ejecutaron varios puntos de referencia para determinar el punto de equilibrio. Y llegó a 85.000 bytes como punto de corte donde la copia ya no mejora el rendimiento. Con una excepción especial para las matrices de double, se consideran "grandes" cuando la matriz tiene más de 1000 elementos. Esa es otra optimización para el código de 32 bits, el asignador de montón de objetos grandes tiene la propiedad especial de que asigna memoria en direcciones que están alineadas con 8, a diferencia del asignador generacional regular que solo asigna alineado con 4. Esa alineación es un gran problema para el doble , leer o escribir un doble mal alineado es muy caro. Curiosamente, la escasa información de Microsoft nunca menciona arreglos largos, no estoy seguro de qué pasa con eso.

Fwiw, hay mucha angustia entre los programadores porque el montón de objetos grandes no se compacta. Esto invariablemente se activa cuando escriben programas que consumen más de la mitad de todo el espacio de direcciones disponible. Seguido mediante el uso de una herramienta como un generador de perfiles de memoria para averiguar por qué el programa falló a pesar de que todavía había mucha memoria virtual sin usar disponible. Dicha herramienta muestra los agujeros en el LOH, trozos de memoria no utilizados donde anteriormente vivía un objeto grande pero se recolectaba la basura. Tal es el precio inevitable del LOH, el agujero solo puede ser reutilizado por una asignación para un objeto que sea de tamaño igual o menor. El verdadero problema es asumir que un programa debería poder consumir toda la memoria virtual en cualquier momento.

Un problema que, de lo contrario, desaparece por completo con solo ejecutar el código en un sistema operativo de 64 bits. Un proceso de 64 bits tiene 8 terabytes de espacio de direcciones de memoria virtual disponible, 3 órdenes de magnitud más que un proceso de 32 bits. No puedes quedarte sin agujeros.

En pocas palabras, LOH hace que el código se ejecute de manera más eficiente. A costa de utilizar un espacio de direcciones de memoria virtual disponible menos eficiente.


ACTUALIZACIÓN, .NET 4.5.1 ahora admite la compactación de la propiedad LOH, GCSettings.LargeObjectHeapCompactionMode . Cuidado con las consecuencias por favor.

Hans Passant
fuente
3
@Hans Passant, ¿podría aclarar sobre el sistema x64, quiere decir que este problema desaparece por completo?
Johnny_D
Algunos detalles de implementación de LOH tienen sentido, pero algunos me desconciertan. Por ejemplo, puedo entender que si se crean y abandonan muchos objetos grandes, generalmente puede ser deseable eliminarlos en masa en una colección Gen2 que por partes en las colecciones Gen0, pero si uno crea y abandona, por ejemplo, una matriz de 22,000 cadenas a las que no existen referencias externas, ¿qué ventaja tiene que las colecciones Gen0 y Gen1 etiqueten todas las 22.000 cadenas como "en vivo" sin tener en cuenta si existe alguna referencia a la matriz?
supercat
6
Por supuesto, el problema de la fragmentación es el mismo en x64. Solo tomará unos días más ejecutar el proceso del servidor antes de que se active.
Lothar
1
Mmmm, no, nunca subestimes 3 órdenes de magnitud. El tiempo que se tarda en recolectar basura en un montón de 4 terabytes es algo que no puede evitar descubrir mucho antes de que se acerque a eso.
Hans Passant
2
@HansPassant ¿Podría, por favor, colaborar en esta afirmación: "Cuánto tiempo se tarda en recolectar basura en un montón de 4 terabytes es algo que no puede evitar descubrir mucho antes de que se acerque a eso".
relativamente_random
9

Si el tamaño del objeto es mayor que algún valor fijado (85000 bytes en .NET 1), CLR lo coloca en el Montón de objetos grandes. Esto optimiza:

  1. Asignación de objetos (los objetos pequeños no se mezclan con los grandes)
  2. Recolección de basura (LOH recolectado solo en GC completo)
  3. Desfragmentación de memoria (LOH se Nunca rara vez compactada)
oleksii
fuente
9

La diferencia esencial de Small Object Heap (SOH) y Large Object Heap (LOH) es que la memoria en SOH se compacta cuando se recopila, mientras que LOH no, como ilustra este artículo . Compactar objetos grandes cuesta mucho. Al igual que en los ejemplos del artículo, digamos que mover un byte en la memoria necesita 2 ciclos, luego compactar un objeto de 8 MB en una computadora de 2 GHz necesita 8 ms, lo cual es un gran costo. Teniendo en cuenta que los objetos grandes (matrices en la mayoría de los casos) son bastante comunes en la práctica, supongo que esa es la razón por la que Microsoft fija objetos grandes en la memoria y propone LOH.

Por cierto, de acuerdo con esta publicación , LOH generalmente no genera problemas de fragmentos de memoria.

uva
fuente
1
La carga de grandes cantidades de datos en objetos administrados suele empequeñecer el costo de 8 ms para compactar el LOH. En la práctica, en la mayoría de las aplicaciones de big data, el costo de LOH es trivial junto al resto del rendimiento de la aplicación.
Shiv
3

El principio es que es poco probable (y posiblemente un mal diseño) que un proceso cree muchos objetos grandes de corta duración, por lo que CLR asigna objetos grandes a un montón separado en el que ejecuta GC en un horario diferente al del montón normal. http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

Myles McDonnell
fuente
Además, colocar objetos grandes en, digamos, la generación 2 podría terminar perjudicando el rendimiento, ya que llevaría mucho tiempo compactar la memoria, especialmente si se libera una pequeña cantidad y los objetos ENORMES tienen que copiarse en una nueva ubicación. El LOH actual no está compactado por razones de rendimiento.
Christopher Currens
Creo que solo es un mal diseño porque el GC no lo maneja bien.
CodesInChaos
@CodeInChaos Aparentemente, hay algunas mejoras en .NET 4.5
Christian.K
1
@CodeInChaos: Si bien puede tener sentido que el sistema espere hasta una colección gen2 antes de intentar recuperar la memoria incluso de objetos LOH de corta duración, no veo ninguna ventaja de rendimiento al declarar objetos LOH (y cualquier objeto que contengan referencias) viven incondicionalmente durante las colecciones gen0 y gen1. ¿Hay algunas optimizaciones que son posibles gracias a tal suposición?
supercat
@supercat Miré el enlace mencionado por Myles McDonnell. Mi entendimiento es: 1. La recolección de LOH ocurre en un GC gen 2. 2. La colección de LOH no incluye compactación (en el momento en que se escribió el artículo). En cambio, marcará los objetos muertos como reutilizables y estos agujeros servirán para futuras asignaciones de LOH si son lo suficientemente grandes. Debido al punto 1, considerando que un GC gen 2 sería lento si hay muchos objetos en gen 2, creo que es mejor evitar usar LOH tanto como sea posible en este caso.
fan de robbie
0

No soy un experto en CLR, pero me imagino que tener un montón dedicado para objetos grandes puede evitar barridos GC innecesarios de los montones generacionales existentes. La asignación de un objeto grande requiere una cantidad significativa de memoria libre contigua . Para proporcionar eso a partir de los "agujeros" dispersos en los montones generacionales, necesitaría compactaciones frecuentes (que solo se realizan con ciclos de GC).

Chris Shain
fuente