scala.collection.concurrent.TrieMapが最低限消費するメモリ量はいくつか

scalametaのbug調査のついでのメモ。

github.com

https://github.com/xuwei-k/scalameta-memory-issue-3650/commit/765dccba2154d1bf90d9c8281ec3e2b7aaa5fa04

TrieMapでcacheするようにしたらしいけれど、それが必要ない時も生成されて、scala/scala 2.13.x branchのtest除いた30万行程度のコードをparseするだけで、(全部のTreeを保持したままにすると)メモリ8G Byteでも足りない、みたいなことになってました。 (あと数割増やすと、どうにか動く)

それでVisualVMの結果をscalametaのissueに貼ったので、そちらの問題に興味がある人はそっち見てもらうとして、TrieMap普通にemptyで生成するだけでも、結構な量使いそうだな?と思ったので、メモ。

VisualVMの出力からも計算できるし、実際にjolなどで計測してもいいですが、つまり以下のような感じで、Scala 2.13.13時点で168 byteだと思われます。

それぞれ1インスタンスあたり

  • 32 byte: AtomicReferenceFieldUpdaterImpl, TrieMap, CNode
  • 24 byte: INode
  • 16 byte: Array[BasicNode], Gen, TrieMap$MangleHashing

合計: (32 * 3) + (24 * 1) + (16 * 3) = 168 byte

issueに貼ったもので

  • TrieMap関連が、それぞれ、大体3732万4259個生成されてる
  • 3732万4259個 * 168 byte = 6270475512 byte => これだけで8GBの大半を占める => そりゃまぁOutOfMemoryErrorになる

という感じで計算も合います。

同じ方法で java.util.concurrent.ConcurrentHashMap 生成すると、おそらく64 byteなので、2.6倍くらい消費する・・・つらい・・・。

    val xs = List.fill(1_000_000)(scala.collection.concurrent.TrieMap.empty[String, String])
    Thread.sleep(1_000_000)

というコード実行してVisualVMで見た例

TrieMap-memory

jolの説明は今回書かないので、以下などを参照

xuwei-k.hatenablog.com

以下、jolでの例

> jol scala.collection.concurrent.TrieMap
scala.collection.concurrent.TrieMap object internals:
OFF  SZ                                                      TYPE DESCRIPTION               VALUE
  0   8                                                           (object header: mark)     N/A
  8   4                                                           (object header: class)    N/A
 12   4                                scala.util.hashing.Hashing TrieMap.hashingobj        N/A
 16   4                                          scala.math.Equiv TrieMap.equalityobj       N/A
 20   4   java.util.concurrent.atomic.AtomicReferenceFieldUpdater TrieMap.rootupdater       N/A
 24   4                                          java.lang.Object TrieMap.root              N/A
 28   4                                                           (object alignment gap)    
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
> jol scala.collection.concurrent.Gen
scala.collection.concurrent.Gen object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     N/A
  8   4        (object header: class)    N/A
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
> jol scala.collection.concurrent.CNode
scala.collection.concurrent.CNode object internals:
OFF  SZ                                      TYPE DESCRIPTION               VALUE
  0   8                                           (object header: mark)     N/A
  8   4                                           (object header: class)    N/A
 12   4      scala.collection.concurrent.MainNode MainNode.prev             N/A
 16   4                                       int CNodeBase.csize           N/A
 20   4                                       int CNode.bitmap              N/A
 24   4   scala.collection.concurrent.BasicNode[] CNode.array               N/A
 28   4           scala.collection.concurrent.Gen CNode.gen                 N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
> jol scala.collection.concurrent.INode
scala.collection.concurrent.INode object internals:
OFF  SZ                                   TYPE DESCRIPTION               VALUE
  0   8                                        (object header: mark)     N/A
  8   4                                        (object header: class)    N/A
 12   4   scala.collection.concurrent.MainNode INodeBase.mainnode        N/A
 16   4        scala.collection.concurrent.Gen INodeBase.gen             N/A
 20   4                       scala.math.Equiv INode.equiv               N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total