Prácticas recomendadas para crear millones de pequeños objetos temporales

109

¿Cuáles son las "mejores prácticas" para crear (y liberar) millones de objetos pequeños?

Estoy escribiendo un programa de ajedrez en Java y el algoritmo de búsqueda genera un único objeto "Mover" para cada movimiento posible, y una búsqueda nominal puede generar fácilmente más de un millón de objetos en movimiento por segundo. La JVM GC ha podido manejar la carga en mi sistema de desarrollo, pero estoy interesado en explorar enfoques alternativos que:

  1. Minimizar la sobrecarga de recolección de basura y
  2. Reducir la huella de memoria máxima para los sistemas de gama baja.

La gran mayoría de los objetos tienen una vida muy corta, pero aproximadamente el 1% de los movimientos generados se conservan y se devuelven como valor persistente, por lo que cualquier técnica de agrupación o almacenamiento en caché tendría que proporcionar la capacidad de excluir objetos específicos para que no se reutilicen. .

No espero un código de ejemplo completo, pero agradecería sugerencias para más lecturas / investigaciones, o ejemplos de código abierto de naturaleza similar.

Programador humilde
fuente
11
¿Sería apropiado el patrón Flyweight para su caso? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland
4
¿Necesitas encapsularlo en un objeto?
nhahtdh
1
El patrón Flyweight no es apropiado porque los objetos no comparten datos comunes significativos. En cuanto a encapsular los datos en un objeto, es demasiado grande para empaquetarlo en una primitiva, por lo que estoy buscando alternativas a los POJO.
Programador humilde

Respuestas:

47

Ejecute la aplicación con recolección de basura detallada:

java -verbose:gc

Y te dirá cuándo se cobrará. Habría dos tipos de barridos, uno rápido y uno completo.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

La flecha es antes y después del tamaño.

Siempre que se trate de GC y no de GC completo, estará a salvo en casa. El GC normal es un coleccionista de copias en la 'generación joven', por lo que los objetos a los que ya no se hace referencia simplemente se olvidan, que es exactamente lo que querría.

Probablemente sea útil leer el ajuste de recolección de basura de la máquina virtual de Java SE 6 HotSpot .

Niels Bech Nielsen
fuente
Experimente con el tamaño del montón de Java para tratar de encontrar un punto donde la recolección de basura completa sea rara. En Java 7, el nuevo G1 GC es más rápido en algunos casos (y más lento en otros).
Michael Shops
21

Desde la versión 6, el modo servidor de JVM emplea una técnica de análisis de escape . Al usarlo, puede evitar la GC por completo.

Mikhail
fuente
1
El análisis de escape a menudo decepciona, vale la pena verificar si la JVM ha descubierto lo que está haciendo o no.
Nitsan Wakart
2
Si tiene experiencia en el uso de estas opciones: -XX: + PrintEscapeAnalysis y -XX: + PrintEliminateAllocations. Sería genial compartirlo. Porque yo no lo digo honestamente.
Mikhail
vea stackoverflow.com/questions/9032519/… necesitará obtener una compilación de depuración para JDK 7, admito que no lo he hecho, pero con JDK 6 ha tenido éxito.
Nitsan Wakart
19

Bueno, ¡aquí hay varias preguntas en una!

1 - ¿Cómo se gestionan los objetos de corta duración?

Como se indicó anteriormente, la JVM puede tratar perfectamente con una gran cantidad de objetos de corta duración, ya que sigue la Hipótesis Generacional Débil .

Tenga en cuenta que estamos hablando de objetos que llegaron a la memoria principal (montón). Este no es siempre el caso. Muchos de los objetos que crea ni siquiera dejan un registro de CPU. Por ejemplo, considere este for-loop

for(int i=0, i<max, i++) {
  // stuff that implies i
}

No pensemos en el desenrollado de bucles (una optimización que la JVM realiza en gran medida en su código). Si maxes igual a Integer.MAX_VALUE, es posible que el ciclo tarde un poco en ejecutarse. sin embargo, eli variable nunca escapará del bloque de bucle. Por lo tanto, la JVM pondrá esa variable en un registro de la CPU, la incrementará regularmente pero nunca la enviará de regreso a la memoria principal.

Por lo tanto, crear millones de objetos no es un gran problema si se usan solo localmente. Estarán muertos antes de ser almacenados en el Edén, por lo que la CG ni siquiera los notará.

2 - ¿Es útil reducir la sobrecarga del GC?

Como de costumbre, depende.

Primero, debe habilitar el registro de GC para tener una visión clara de lo que está sucediendo. Puede habilitarlo con -Xloggc:gc.log -XX:+PrintGCDetails.

Si su aplicación pasa mucho tiempo en un ciclo de GC, entonces sí, ajuste el GC; de lo contrario, es posible que no valga la pena.

Por ejemplo, si tiene un GC joven cada 100 ms que tarda 10 ms, pasa el 10% de su tiempo en el GC y tiene 10 colecciones por segundo (lo que es enorme). En tal caso, no dedicaría tiempo a ajustar el GC, ya que esos 10 GC / s todavía estarían allí.

3 - Algo de experiencia

Tuve un problema similar en una aplicación que estaba creando una gran cantidad de una clase determinada. En los registros de GC, noté que la tasa de creación de la aplicación era de alrededor de 3 GB / s, que es demasiado (vamos ... ¡¿3 gigabytes de datos por segundo?!).

El problema: demasiados GC frecuentes provocados por la creación de demasiados objetos.

En mi caso, adjunté un generador de perfiles de memoria y noté que una clase representaba un gran porcentaje de todos mis objetos. Rastreé las instancias para descubrir que esta clase era básicamente un par de valores booleanos envueltos en un objeto. En ese caso, había dos soluciones disponibles:

  • Vuelva a trabajar el algoritmo para que no devuelva un par de booleanos, sino que tenga dos métodos que devuelvan cada booleano por separado

  • Almacene los objetos en caché, sabiendo que solo había 4 instancias diferentes

Elegí el segundo, ya que tuvo el menor impacto en la aplicación y fue fácil de introducir. Me tomó minutos poner una fábrica con un caché no seguro para subprocesos (no necesitaba seguridad para subprocesos, ya que eventualmente tendría solo 4 instancias diferentes).

La tasa de asignación bajó a 1 GB / s, al igual que la frecuencia de GC joven (dividida por 3).

Espero que ayude !

Pierre Laporte
fuente
11

Si solo tiene objetos de valor (es decir, sin referencias a otros objetos) y realmente, pero me refiero a toneladas y toneladas de ellos, puede usar directamente ByteBufferscon el orden de bytes nativo [lo último es importante] y necesita algunos cientos de líneas de código para asignar / reutilizar + getter / setters. Getters se parecen along getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Eso resolvería el problema de GC casi por completo siempre que asigne una sola vez, es decir, una gran parte y luego administre los objetos usted mismo. En lugar de referencias, solo tendría un índice (es decir, int) en el ByteBufferque debe transmitirse. Es posible que también deba alinear la memoria usted mismo.

La técnica se sentiría como si se usara C and void*, pero con un poco de envoltura es soportable. Un inconveniente del rendimiento podría ser la comprobación de límites si el compilador no lo elimina. Una ventaja importante es la localidad: si procesa las tuplas como vectores, la falta del encabezado del objeto también reduce la huella de memoria.

Aparte de eso, es probable que no necesite un enfoque de este tipo, ya que la generación joven de prácticamente todas las JVM muere trivialmente y el costo de asignación es solo un aumento de puntero. El costo de asignación puede ser un poco más alto si usa finalcampos, ya que requieren un límite de memoria en algunas plataformas (a saber, ARM / Power), aunque en x86 es gratis.

mejor
fuente
8

Suponiendo que encuentre que GC es un problema (como otros señalan que podría no serlo), implementará su propia administración de memoria para su caso especial, es decir, una clase que sufre una pérdida masiva. Prueba la agrupación de objetos, he visto casos en los que funciona bastante bien. La implementación de grupos de objetos es un camino muy transitado, por lo que no es necesario volver a visitar aquí, esté atento a:

  • subprocesos múltiples: el uso de grupos locales de subprocesos podría funcionar para su caso
  • estructura de datos de respaldo: considere usar ArrayDeque, ya que funciona bien en la eliminación y no tiene gastos generales de asignación
  • limita el tamaño de tu piscina :)

Mida antes / después, etc., etc.

Nitsan Wakart
fuente
6

Me encontré con un problema similar. En primer lugar, intente reducir el tamaño de los objetos pequeños. Introdujimos algunos valores de campo predeterminados haciendo referencia a ellos en cada instancia de objeto.

Por ejemplo, MouseEvent tiene una referencia a la clase Point. Almacenamos en caché los puntos y los referenciamos en lugar de crear nuevas instancias. Lo mismo para, por ejemplo, cadenas vacías.

Otra fuente fueron múltiples booleanos que fueron reemplazados por un int y para cada booleano usamos solo un byte del int.

StanislavL
fuente
Solo por interés: ¿Qué te compró en cuanto a rendimiento? ¿Hizo un perfil de su aplicación antes y después del cambio y, de ser así, cuáles fueron los resultados?
Axel
@Axel los objetos usan mucha menos memoria por lo que GC no se llama con tanta frecuencia. Definitivamente perfilamos nuestra aplicación, pero hubo incluso un efecto visual de la velocidad mejorada.
StanislavL
6

Me ocupé de este escenario con algún código de procesamiento XML hace algún tiempo. Me encontré creando millones de objetos de etiquetas XML que eran muy pequeños (generalmente solo una cadena) y de muy corta duración (la falla de una verificación XPath significaba que no había coincidencias, así que descarte).

Hice algunas pruebas serias y llegué a la conclusión de que solo podía lograr una mejora del 7% en la velocidad utilizando una lista de etiquetas descartadas en lugar de crear otras nuevas. Sin embargo, una vez implementada, descubrí que la cola libre necesitaba un mecanismo agregado para podarla si se volvía demasiado grande; esto anuló completamente mi optimización, así que la cambié a una opción.

En resumen, probablemente no valga la pena, pero me alegra ver que estás pensando en ello, demuestra que te preocupas.

ViejoCurmudgeon
fuente
2

Dado que está escribiendo un programa de ajedrez, existen algunas técnicas especiales que puede utilizar para obtener un rendimiento decente. Un enfoque simple es crear una gran variedad de longs (o bytes) y tratarlo como una pila. Cada vez que su generador de movimientos crea movimientos, coloca un par de números en la pila, por ejemplo, se mueve de una casilla a otra. A medida que evalúe el árbol de búsqueda, irá sacando movimientos y actualizando una representación del tablero.

Si quieres usar objetos de poder expresivo. Si quieres velocidad (en este caso) hazlo nativo.

David Plumpton
fuente
1

Una solución que he usado para tales algoritmos de búsqueda es crear solo un objeto Move, mutarlo con un nuevo movimiento y luego deshacer el movimiento antes de dejar el alcance. Probablemente esté analizando solo un movimiento a la vez y luego simplemente almacenando el mejor movimiento en algún lugar.

Si eso no es factible por alguna razón, y desea disminuir el uso máximo de memoria, un buen artículo sobre la eficiencia de la memoria está aquí: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf

rkj
fuente
Enlace muerto. ¿Hay otra fuente para ese artículo?
dnault
0

Simplemente cree sus millones de objetos y escriba su código de la manera adecuada: no guarde referencias innecesarias a estos objetos. GC hará el trabajo sucio por usted. Puede jugar con GC detallado como se mencionó para ver si realmente están GC'd. Java ES sobre la creación y liberación de objetos. :)

Gyorgyabraham
fuente
1
Lo siento amigo, no estoy de acuerdo con su enfoque ... Java, como cualquier lenguaje de programación, se trata de resolver un problema dentro de sus limitaciones, si el OP está limitado por GC, ¿cómo lo está ayudando?
Nitsan Wakart
1
Le estoy contando cómo funciona realmente Java. Si no puede esquivar la situación de tener millones de objetos temporales, el mejor consejo podría ser, la clase temporal debería ser ligera y debe asegurarse de publicar las referencias lo antes posible, ni un solo paso más. ¿Me estoy perdiendo de algo?
gyorgyabraham
Java admite la creación de basura y la limpiaría por ti, eso es cierto. Si el OP no puede esquivar la creación de objetos y no está satisfecho con el tiempo que pasa en GC, es un final triste. Mi objeción es la recomendación que hace de hacer más trabajo para GC porque de alguna manera es Java adecuado.
Nitsan Wakart
0

Creo que debería leer sobre la asignación de pilas en Java y el análisis de escape.

Porque si profundiza en este tema, puede encontrar que sus objetos ni siquiera están asignados en el montón, y GC no los recopila de la misma manera que los objetos en el montón.

Hay una explicación de wikipedia del análisis de escape, con un ejemplo de cómo funciona esto en Java:

http://en.wikipedia.org/wiki/Escape_analysis

luke1985
fuente
0

No soy un gran admirador de GC, así que siempre trato de encontrar formas de evitarlo. En este caso, sugeriría usar el patrón Object Pool :

La idea es evitar la creación de nuevos objetos almacenándolos en una pila para poder reutilizarlos más tarde.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
Ilya Gazman
fuente
3
Usar pool para objetos pequeños es una mala idea, necesitas un pool por hilo para arrancar (o el acceso compartido mata cualquier rendimiento). Estos grupos también funcionan peor que un buen recolector de basura. Por último: el GC es un regalo del cielo cuando se trata con código / estructuras concurrentes; muchos algoritmos son significativamente más fáciles de implementar ya que, naturalmente, no hay ningún problema de ABA. Árbitro. contar en un entorno concurrente requiere al menos una operación atómica + cerca de memoria (LOCK ADD o CAS en x86)
bestsss
1
La administración de objetos en el grupo puede ser más costosa que dejar que se ejecute el recolector de basura.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen En general, estoy de acuerdo con usted, pero tenga en cuenta que detectar tal diferencia es todo un desafío, y cuando llega a la conclusión de que GC funciona mejor en su caso, debe ser un caso único si tal diferencia importa. Como sea al revés, es posible que el grupo de objetos guarde su aplicación.
Ilya Gazman
1
¿Simplemente no entiendo tu argumento? ¿Es muy difícil detectar si GC es más rápido que la agrupación de objetos? ¿Y, por tanto, debería utilizar la agrupación de objetos? La JVM está optimizada para codificación limpia y objetos de corta duración. Si de eso se trata esta pregunta (que espero si OP genera un millón de ellos por segundo), entonces solo debería ser si hay una ventaja demostrable para cambiar a un esquema más complejo y propenso a errores como el que sugiere. Si esto es demasiado difícil de probar, entonces para qué molestarse.
Thorbjørn Ravn Andersen
0

Los grupos de objetos proporcionan mejoras tremendas (en ocasiones 10 veces) sobre la asignación de objetos en el montón. ¡Pero la implementación anterior usando una lista vinculada es ingenua y incorrecta! La lista enlazada crea objetos para gestionar su estructura interna anulando el esfuerzo. Un Ringbuffer que usa una variedad de objetos funciona bien. En el ejemplo, dar (un programa de ajedrez que gestiona movimientos) el Ringbuffer debe estar envuelto en un objeto de soporte para la lista de todos los movimientos calculados. Solo se pasarían entonces las referencias de objeto del titular de movimientos.

Michael Röschter
fuente