Eden Space
Entonces, mi pregunta es si algo de esto puede ser realmente cierto, y si es así, ¿por qué la asignación del montón de Java es mucho más rápida?
He estado estudiando un poco sobre cómo funciona el GC de Java, ya que es muy interesante para mí. Siempre estoy tratando de expandir mi colección de estrategias de asignación de memoria en C y C ++ (interesado en tratar de implementar algo similar en C), y es una forma muy, muy rápida de asignar muchos objetos de forma explosiva desde un perspectiva práctica pero principalmente debido a la multiproceso.
La forma en que funciona la asignación de Java GC es utilizar una estrategia de asignación extremadamente barata para asignar inicialmente objetos al espacio "Eden". Por lo que puedo decir, está usando un asignador secuencial de grupos.
Eso es mucho más rápido solo en términos de algoritmo y reducción de fallas de página obligatorias que las de uso general malloc
en C o por defecto, agregando operator new
C ++.
Pero los asignadores secuenciales tienen una debilidad evidente: pueden asignar fragmentos de tamaño variable, pero no pueden liberar ningún fragmento individual. Simplemente asignan de forma secuencial directa con relleno para alineación, y solo pueden purgar toda la memoria que asignaron a la vez. Por lo general, son útiles en C y C ++ para construir estructuras de datos que solo necesitan inserciones y no eliminaciones de elementos, como un árbol de búsqueda que solo necesita construirse una vez cuando se inicia un programa y luego se busca repetidamente o solo se agregan nuevas claves ( sin llaves quitadas).
También se pueden usar incluso para estructuras de datos que permiten que se eliminen elementos, pero esos elementos en realidad no se liberarán de la memoria ya que no podemos desasignarlos individualmente. Dicha estructura que usa un asignador secuencial solo consumiría más y más memoria, a menos que tuviera un pase diferido donde los datos se copiaron en una copia nueva y compacta usando un asignador secuencial separado (y eso a veces es una técnica muy efectiva si un asignador fijo ganara hágalo por alguna razón: simplemente asigne secuencialmente una nueva copia de la estructura de datos y descargue toda la memoria de la anterior).
Colección
Al igual que en el ejemplo de estructura de datos / grupo secuencial anterior, sería un gran problema si Java GC solo se asigna de esta manera, aunque es súper rápido para una asignación de ráfaga de muchos fragmentos individuales. No podría liberar nada hasta que se cierre el software, momento en el que podría liberar (purgar) todos los grupos de memoria de una sola vez.
Entonces, en cambio, después de un solo ciclo de GC, se hace un pase a través de los objetos existentes en el espacio "Eden" (asignado secuencialmente), y los que todavía están referenciados luego se asignan usando un asignador de propósito más general capaz de liberar fragmentos individuales. Los que ya no están referenciados simplemente serán desasignados en el proceso de purga. Básicamente, es "copiar objetos del espacio del Edén si todavía están referenciados y luego purgarlos".
Esto normalmente sería bastante costoso, por lo que se realiza en un subproceso de fondo separado para evitar detener significativamente el subproceso que originalmente asignó toda la memoria.
Una vez que la memoria se copia del espacio de Eden y se asigna utilizando este esquema más costoso que puede liberar fragmentos individuales después de un ciclo de GC inicial, los objetos se mueven a una región de memoria más persistente. Esos trozos individuales se liberan en los siguientes ciclos de GC si dejan de ser referenciados.
Velocidad
Entonces, en términos generales, la razón por la que el GC de Java podría superar a C o C ++ en la asignación directa de almacenamiento dinámico es porque está utilizando la estrategia de asignación más barata y totalmente degeneralizada en el subproceso que solicita asignar memoria. Luego ahorra el trabajo más costoso que normalmente tendríamos que hacer cuando usamos un asignador más general como el directomalloc
para otro hilo.
Conceptualmente, el GC en realidad tiene que hacer más trabajo en general, pero lo distribuye a través de subprocesos para que el costo total no se pague por adelantado por un solo subproceso. Permite que el hilo que asigna memoria lo haga súper barato, y luego difiere el gasto real requerido para hacer las cosas correctamente para que los objetos individuales puedan liberarse a otro hilo. En C o C ++ cuando llamamos malloc
o operator new
tenemos que pagar el costo total por adelantado dentro del mismo hilo.
Esta es la principal diferencia, y por qué Java podría superar a C o C ++ usando llamadas ingenuas malloc
o operator new
asignar un montón de fragmentos pequeños individualmente. Por supuesto, típicamente habrá algunas operaciones atómicas y algunos bloqueos potenciales cuando se inicie el ciclo GC, pero probablemente esté optimizado bastante.
Básicamente, la explicación simple se reduce a pagar un costo más alto en un solo hilo ( malloc
) versus pagar un costo más barato en un solo hilo y luego pagar el costo más alto en otro que puede ejecutarse en paralelo ( GC
). Como inconveniente, hacer las cosas de esta manera implica que se requieren dos direcciones indirectas para ir de la referencia del objeto al objeto, según sea necesario, para permitir que el asignador copie / mueva la memoria sin invalidar las referencias existentes del objeto, y también puede perder la ubicación espacial una vez que la memoria del objeto es se mudó del espacio "Edén".
Por último, pero no menos importante, la comparación es un poco injusta porque el código C ++ normalmente no asigna una gran cantidad de objetos individualmente en el montón. El código C ++ decente tiende a asignar memoria para muchos elementos en bloques contiguos o en la pila. Si asigna un bote lleno de pequeños objetos uno a la vez en la tienda gratuita, el código es simple.