funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

Velikost objektů na JVM - Scala a specialiazce polí

20. 2. 2013 — k47

Minule jsem napsal pár rych­lých řádků o ve­li­kosti spe­ci­a­li­zo­va­ných tříd. Vý­sle­dek je takový, že i když se ve­li­kost třídy o něco zvětší, ve vý­sledku ušet­řím paměť, pro­tože ne­mu­sím od­ka­zo­vat na bo­xo­vané ob­jekty (In­te­ger a spol).

Si­tu­ace kolem spe­ci­a­li­zo­va­ných polí je o něco kom­pli­ko­va­nější pro­tože kromě ano­tace @specialized ještě po­tře­buji im­pli­citní ClassManifest (nebo ClassTag jak se tomu říká ve Scale 2.10).

class ArrayList[@specialized(Int, Long) T: ClassManifest] {
  val elements = new Array[T](16)
}

Důvody jsou jasné:

new Array[T](len) /* se přeloží na: */ classManifest.newArray(len)

// bez specializace se kód překládá následovně:
arr(idx)          /*  ~~~~~~~~~~~>  */ ScalaRunTime.array_apply(arr, idx)
arr(idx) = x                           ScalaRunTime.array_update(arr, idx, x)
arr.length                             ScalaRunTime.array_length(arr)

Takže asi tak.

Ale je tu ještě ně­ko­lik zá­drhelů.

Spe­ci­a­li­zo­vaná třída dědí ze svého ne­spe­ci­a­li­zo­va­ného předka a při­dává atri­but pri­mi­tiv­ního typu se kterým pra­cuje, při­čemž ge­ne­ric­kého atri­butu předka se ne­do­tkne (de­tailně jsem to popsal minule). Tak to fun­guje i v pří­padě polí. Předek ob­sa­huje re­fe­renci na ge­ne­rické pole, spe­ci­a­li­zo­vaný po­to­mek přidá re­fe­renci na pri­mi­tivní pole. Háček je v tom, že se ini­ci­a­li­zují oba atri­buty. Nejdřív se zavolá kon­struk­tor předka, který ini­ci­a­li­zuje ge­ne­rický atri­but a pak kon­struk­tor po­tomka, který ini­ci­a­li­zuje svůj spe­ci­a­li­zo­vaný atri­but. Pokud je v kon­struk­toru třídy spe­ci­a­li­zo­vané pro T něco jako val elements = new Array[T](16), tak se ini­ci­a­li­zují dvě pole.

Výše uve­dená třída se zkom­pi­luje jako:

class ArrayList[T](ev: ClassManifest[T]) {
  private[this] val evidence$1: ClassManifest[T] = ev
  val elements = new Array[AnyRef](16)
}

class ArrayList$mcI$sp extends X {
  private[this] val evidence$1: ClassManifest[T] = ev
  var elements$mcI$sp: Array[Int] = evidence$1.newArray(16)
}

Nejdřív se ini­ci­a­li­zuje pro­měnná elements předka a pak elements$mcI$sp po­tomka. A to je pro­blém. Pole re­fe­rencí s délkou 16 zabírá 88 bajtů (na 32 bit ar­chi­tek­tu­rách, na 64 bi­to­vých je to 2x horší), které ne­bu­dou nikdy po­u­žity, pro­tože spe­ci­a­li­zo­vaný po­to­mek po­u­žívá svůj atri­but elements$mcI$sp.

Proto se vy­platí v kon­struk­toru spe­ci­a­li­zo­vané pole ini­ci­a­li­zo­vat na null a pole alo­ko­vat až v nějaké metodě:

class ArrayList[@specialized(Int, Long) T: ClassManifest] {
  val elements = _

  def add(t: T) = {
    if (elements == null) elements = new Array[T](16)
    // ...
  }
}

Další pro­blém spo­čívá v ClassMa­ni­fes­tech. Pokud ho de­fi­nuji jako type bound (tedy T: ClassManifest) kom­pi­lá­tor vy­ge­ne­ruje pri­vátní pro­měn­nou jak pro ge­ne­ric­kého předka tak i pro spe­ci­a­li­zo­va­ného po­tomka, což zna­mená další 4 nebo 8 zby­tečně vy­plýtva­ných bajtů na in­stanci. Na druhou stranu, když im­pli­citní ma­ni­fest de­kla­ruji jako pa­ra­metr implicit val m: ClassManifest[T], pak pro něj kom­pi­lá­tor vy­ge­ne­ruje ve­řej­nou/pro­tec­ted pro­měn­nou, která je sdí­lená před­kem a po­tomky.

Další věc, kterou je po­třeba mít na paměti je mo­di­fi­ká­tor vi­di­tel­nosti private[this]. Ten způ­sobí, že atri­but je vi­di­telný jenom této in­stanci a na rozdíl od mo­di­fi­ká­toru this ne­ge­ne­ruje sou­kromý getter nebo setter. Bo­hu­žel tohle velice bi­zar­ním způ­so­bem vadí spe­ci­a­li­zaci. Jde o to, že private[this] atri­buty nejsou ve spe­ci­a­li­zo­va­ných me­to­dách na­hra­zeny spe­ci­a­li­zo­va­nými atri­buty a cel­kově kód ne­fun­guje jak chci a vůbec není znát proč.

Takže pokud do­dr­žím všechna pra­vi­dla: ano­tace @specialized, ClassManifest jako im­pli­citní val, ini­ci­a­li­zo­vat na null a žádný private[this], můžu dostat in­stanci se spe­ci­a­li­zo­va­ným polem, které mám může ušet­řit značné množ­ství paměti (na 64 bit plat­for­mách po­tře­buji k ulo­žení jed­noho intu 4 bajty na­místo 32, tedy 8x méně). Můžu na­pří­klad vy­tvo­řit obdobu Ja­vov­ského Arra­y­Listu, která je (ko­nečně) pa­mě­ťově efek­tivní.

class ArrayList[@specialized(Short, Int, Long, Float, Double) T](implicit val cl: ClassManifest[T]) {
  private var arr: Array[T] = _
  private var len = 0

  def length = len

  def append(t: T): Unit = {
    if (arr == null) arr = new Array[T](16)
    if (len == arr.length) enlarge()
    arr(len) = t
    len += 1
  }

  def get(i: Int) = {
    if (i < 0 || i >= len) throw new IndexOutOfBoundsException
    arr(i)
  }

  private def enlarge() = {
    val newArr = new Array[T](arr.length * 2)
    Array.copy(arr, 0, newArr, 0, arr.length)
    arr = newArr
  }
}

A když se po­dí­vám na spo­třebu paměti, je kon­stantní faktor spe­ci­a­li­zo­va­ných in­stancí pře­kva­pivě malý (zčásti kvůli za­rov­nání paměti):

Spe­ci­a­li­zo­vaná pole se v ta­ko­vém pří­padě vy­platí už pro mi­ni­a­turní ko­lekce. Bo­hu­žel fra­mework ko­lekcí ve Scale není vůbec spe­ci­a­li­zo­vaný (zkuste si ve zdro­já­cích Scaly grepnout @spe­ci­a­li­zed a uvi­díte, že to ne­vrátí skoro nic), takže i kdyby můj Arra­y­List uklá­dal data v in­ter­ních pri­mi­tiv­ních polích, přesto by vět­ši­nou pra­co­val s bo­xo­va­nými typy. To ale je jenom malá vada na kráse dras­ticky zre­du­ko­va­nému hladu po paměti.

@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články