Как уменьшить количество объектов, созданных в Scala?

Я программирую приложение компьютерной графики в Scala, которое использует класс RGB для возврата цвета в точку на изображении. Как вы можете себе представить, функция, возвращающая объект RGB цвета, вызывается много раз.

class RGB(val red: Int, val green: Int, val blue: Int) { }

Существует функция getPixelRGB, которая часто используется следующим образом

val color:RGB = getPixelRGB(image, x, y)

Проблема в том, что я могу назвать эту функцию миллион раз, которая, как я полагаю, создаст миллион уникальных экземпляров объектов RGB, что очень непривлекательно. Есть некоторые мысли, которые я имею об этом:

  • getPixelRGB может потенциально создать бесконечное число объектов, если оно было вызвано бесконечным числом раз, но это не должно быть бесконечное число объектов, так как существует только 255 255 255 возможных комбинаций, которые могут быть созданы для RGB. Таким образом, количество объектов, созданных "должно", должно быть конечным. Эта функция может быть скорректирована для использования пула объектов, где, если он должен возвращать тот же цвет, что и некоторое время, прежде чем он сможет вернуть один и тот же экземпляр объединенного объекта для этого цвета.

  • Я могу кодировать этот RGB как Int. У Int будет меньше накладных расходов памяти, чем обычный объект Scala/Java, объекты Java имеют дополнительные издержки памяти. Поскольку тип Scala Int имеет ширину 4 байта, первые 3 байта могут сохранять значение RGB. Я предполагаю, что возврат только Int, а не RGB из метода getPixelRGB будет меньше. Однако как это сделать, все еще имея убеждение класса RGB?

  • Предположительно, и это короткие объекты, и я прочитал, что сборщик мусора должен повторно потребовать их быстро. Однако я все еще беспокоюсь об этом. Как GC знает, что я быстро выброшу его? Так запутанно.

Итак, в общем, мой вопрос заключается в том, как сделать этот getPixelRGB более дружественным к памяти? также я должен даже беспокоиться об этом?

Ответ 1

Вы можете закодировать RGB с одним длинным или int. Более того, в scala 2.10 вы можете определить класс значений для примитивных значений, скажем

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)*/
}

С классом значений вы все еще можете иметь информацию о типе и наслаждаться распределением памяти без классов в JVM.

Ответ 2

Ваш вопрос №3 еще не был рассмотрен, поэтому я дам ему шанс.

Как GC знает, что я быстро выброшу [короткоживущие объекты]?

Работа современных ГЦ основана на наблюдении, что объекты разного времени жизни ведут себя по-разному. Таким образом, он управляет ими в так называемых поколениях. Созданные объекты сохраняются в пространстве eden. Когда это заполняется, все объекты в нем, на которые все еще ссылаются (т.е. Они живы), копируются на так называемое пространство молодого поколения. Таким образом, все мертвые объекты остаются позади, а занимаемое им пространство возвращается с практически нулевым усилием. Это то, что делает короткоживущие объекты такими дешевыми для JVM. И большинство объектов, созданных средней программой, являются временными или локальными переменными, которые очень быстро выпадают из области действия.

После этого первого раунда GC пространство молодого поколения управляется аналогичным образом, за исключением того, что их может быть больше. GC может быть сконфигурирован так, чтобы объекты тратили один или несколько раундов в пространстве (-ях) молодого поколения. Затем, в конечном счете, конечные выжившие мигрируют в пространство для оставшихся в живых (иначе старое поколение), где они должны оставаться на всю оставшуюся жизнь. Это пространство управляется путем периодического применения некоторого варианта классической техники меток и развертки: пройдитесь по графику всех живых ссылок и отметьте живые объекты, затем выметьте все немаркированные (мертвые) объекты, уплотняя оставшихся в живых в один непрерывный блок памяти, таким образом дефрагментация свободной памяти. Это дорогостоящая операция, которая блокирует выполнение программы, и ее очень сложно реализовать правильно, особенно в современной многопоточной виртуальной машине. Именно поэтому был изобретен генераторный GC, чтобы гарантировать, что на этот этап попадает только малая часть всех объектов.

Ответ 3

В плане удобства использования памяти наиболее эффективным решением является сохранение полной информации о цвете только в одном Int. Как вы уже упоминали, информация о цвете требует всего три байта, поэтому четырех байтов Int достаточно. Вы можете кодировать и декодировать информацию RGB из одного Int с помощью битовых операций:

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)
)

Ответ 4

Предположительно, и это короткие объекты, и я прочитал, что сборщик мусора должен повторно потребовать их быстро. Однако я все еще беспокоюсь об этом. Как GC знает, что я быстро выброшу его? Так запутанно.

Он этого не знает. Он предполагает это. Это называется гипотезой поколений, на которой построены все коллекторы сборщиков мусора:

  • почти все объекты умирают молодыми
  • почти нет старых объектов, содержащих ссылки на новые объекты

Объекты, которые удовлетворяют этой гипотезе, очень дешевы (даже более дешевые, чем malloc и free в таких языках, как C), только объекты, которые нарушают одно или оба предположения, являются дорогостоящими.

Ответ 5

У вас может быть интерфейс, который возвращает простой Int. Затем вы можете использовать неявные преобразования для обработки Int как объекта RGB, если это необходимо.

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)
  }

}

Затем вы можете рассматривать любой Int как RGBInt, где вы думаете, что это имеет смысл:

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
}