example - ¿Cómo reducir el número de objetos creados en Scala?
java memory management (5)
Supuestamente, y son objetos de vida corta, y he leído que el recolector de basura debería reclamarlos rápidamente. Sin embargo, todavía estoy preocupado por eso. ¿Cómo sabe el GC que lo estoy tirando rápidamente? Muy confuso.
No lo sabe Lo asume . Esto se llama la hipótesis generacional en la que se construyen todos los recolectores de basura generacionales:
- casi todos los objetos mueren jóvenes
- casi ningún objeto antiguo contiene referencias a objetos nuevos
Los objetos que satisfacen esta hipótesis son muy baratos (incluso más baratos, de hecho, que malloc
y free
en idiomas como C), solo los objetos que violan uno o ambos supuestos son costosos.
Estoy programando una aplicación de gráficos por computadora en Scala que usa una clase RGB para devolver el color en un punto de la imagen. Como se puede imaginar, la función que devuelve el objeto RGB de color se llama muchas veces.
class RGB(val red: Int, val green: Int, val blue: Int) { }
Hay una función getPixelRGB que a menudo se usa de la siguiente manera
val color:RGB = getPixelRGB(image, x, y)
El problema es que puedo llamar a esta función un millón de veces, lo que generará un millón de instancias de objetos RGB únicas, creo que es una situación muy poco atractiva. Hay algunos pensamientos que tengo sobre esto:
getPixelRGB podría crear un número infinito de objetos si se llamara un número infinito de veces, pero no tiene que ser un número infinito de objetos, ya que solo hay un máximo de 255 * 255 * 255 combinaciones posibles que pueden producirse para RGB. Entonces la cantidad de objetos creados "debería" ser finita. Esta función podría ajustarse para usar un grupo de objetos donde devolverá el mismo color antes de que pueda devolver la misma instancia del objeto agrupado para ese color.
Podría codificar este RGB como Int. Un Int tendría menos sobrecarga de memoria que un objeto Scala / Java normal, los objetos Java tienen una sobrecarga de memoria adicional. Como un tipo de Scala Int tiene 4 bytes de ancho, los primeros 3 bytes podrían almacenar el valor RGB. Solo devolver un int en lugar de un RGB del método getPixelRGB supondría menos sobrecarga de memoria, supongo. Sin embargo, ¿cómo hacer esto sin dejar de convencer a la clase RGB?
Supuestamente, y son objetos de vida corta, y he leído que el recolector de basura debería reclamarlos rápidamente. Sin embargo, todavía estoy preocupado por eso. ¿Cómo sabe el GC que lo estoy tirando rápidamente? Muy confuso.
Entonces, en general, mi pregunta es cómo hacer que getPixelRGB sea más amigable con la memoria. también debería preocuparme por eso?
En términos de facilidad de memoria, la solución más eficiente es almacenar la información de color completa en un solo Int. Como ha mencionado correctamente, la información de color requiere solo tres bytes, por lo que los cuatro bytes de Int son suficientes. Puede codificar y decodificar la información RGB de un Int utilizando operaciones de bits:
def toColorCode(r: Int, g: Int, b: Int) = r << 16 | g << 8 | b
def toRGB(code: Int): (Int, Int, Int) = (
(code & 0xFF0000) >> 16,
(code & 0x00FF00) >> 8,
(code & 0x0000FF)
)
Podría tener una interfaz que devuelva un simple Int
. Entonces podría usar conversiones implícitas para tratar un Int
como un objeto RGB
donde sea necesario.
case class RBGInt(red: Int, green: Int, blue: Int) {
// ...
}
object Conversions {
implicit def toRGBInt(p: Int) = {
val (r, g, b) = /* some bitmanipulation to turn p into 3 ints */
RGBInt(r, g, b)
}
}
Entonces podrías tratar cualquier Int
como un RGBInt
donde creas que tiene sentido:
type RGB = Int // useful in documenting interfaces that consume
// or returns Ints which represent RGBs
def getPixelRGB(img: Image, x: Int, y: Int): RGB = {
// returns an Int
}
def someMethod(..) = {
import Conversions._
val px: RGB = getPixelRGB(...) // px is actually an Int
px.red // px, an Int is lifted to an RGBInt
}
Puede codificar RGB con single long o int . Además, en scala 2.10 puedes definir la clase de valor para valores primitivos, por ejemplo
class RGB private(val underlying: Long) extends AnyVal {
def toTriple = /*decoding to (red, green, blue)*/
}
object RGB {
def apply(red: Int, green: Int, blue: Int) = /* encode and create class with new RGB(longvalue)*/
}
Con la clase de valor, aún puede tener información de tipo y disfrutar de un diseño de memoria sin clases en JVM.
Su pregunta n. ° 3 aún no fue abordada, así que le daré una oportunidad.
¿Cómo sabe el GC que estoy tirando [objetos de corta duración] rápidamente?
El funcionamiento de los GC modernos se basa en la observación de que los objetos de distinta duración se comportan de forma muy diferente. Entonces los maneja en las llamadas generaciones . Los objetos recién creados se almacenan en el espacio eden . Cuando esto se llena, todos los objetos que aún están siendo referenciados (es decir, están vivos) se copian en el llamado espacio de generación joven . Por lo tanto, todos los objetos muertos quedan atrás y el espacio ocupado por ellos se recupera con prácticamente cero esfuerzo. Esto es lo que hace que los objetos efímeros sean tan baratos para la JVM. Y la mayoría de los objetos creados por un programa promedio son variables temporales o locales que quedan fuera del alcance muy rápidamente.
Después de esta primera ronda de GC, los espacios de generación joven se administran de manera similar, excepto que puede haber más de ellos. El GC se puede configurar para que los objetos pasen una o más rondas en el (los) espacio (s) de la generación joven. Luego, finalmente, los supervivientes finales son migrados al espacio del sobreviviente (también conocido como la generación anterior ), donde permanecerán por el resto de sus vidas. Este espacio se gestiona aplicando periódicamente alguna variante de la técnica clásica de marca y barrido : recorra el gráfico de todas las referencias en vivo y marque objetos en vivo, luego barra todos los objetos no marcados (muertos) compactando a los supervivientes en un bloque de memoria contiguo, por lo tanto desfragmentando la memoria libre. Esta es una operación costosa que bloquea la ejecución del programa, y es muy difícil implementarla correctamente, especialmente en una VM moderna con múltiples subprocesos. Esta es la razón por la que se inventó el GC generacional, para garantizar que solo una pequeña fracción de todos los objetos creados llegue a esta etapa.