stringlistscalascala-collectionsmemory-consumption

Higher memory allocation rates for List of single-character String than multi-character String


Consider the following benchmark which allocates List of String of length 1 versus of length 8

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class SoMemory {
  val size = 1_000_000
  @Benchmark def a: List[String] = List.fill[String](size)(Random.nextString(1))
  @Benchmark def b: List[String] = List.fill[String](size)(Random.nextString(8))
}

where sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 -prof gc bench.SoMemory" gives

[info] Benchmark                                     Mode  Cnt           Score          Error   Units
[info] SoMemory.a                                   thrpt   20          16.650 ±        0.519   ops/s
[info] SoMemory.a:·gc.alloc.rate                    thrpt   20        3870.364 ±      120.687  MB/sec
[info] SoMemory.a:·gc.alloc.rate.norm               thrpt   20   255963282.822 ±       61.012    B/op
[info] SoMemory.a:·gc.churn.PS_Eden_Space           thrpt   20        3862.090 ±      161.598  MB/sec
[info] SoMemory.a:·gc.churn.PS_Eden_Space.norm      thrpt   20   255331784.446 ±  4839869.981    B/op
[info] SoMemory.a:·gc.churn.PS_Survivor_Space       thrpt   20          25.893 ±        1.433  MB/sec
[info] SoMemory.a:·gc.churn.PS_Survivor_Space.norm  thrpt   20     1711320.051 ±    64870.177    B/op
[info] SoMemory.a:·gc.count                         thrpt   20         318.000                 counts
[info] SoMemory.a:·gc.time                          thrpt   20       45183.000                     ms
[info] SoMemory.b                                   thrpt   20           2.859 ±        0.092   ops/s
[info] SoMemory.b:·gc.alloc.rate                    thrpt   20        2763.961 ±       89.654  MB/sec
[info] SoMemory.b:·gc.alloc.rate.norm               thrpt   20  1063705990.899 ±      503.169    B/op
[info] SoMemory.b:·gc.churn.PS_Eden_Space           thrpt   20        2768.433 ±      101.742  MB/sec
[info] SoMemory.b:·gc.churn.PS_Eden_Space.norm      thrpt   20  1065601049.380 ± 25878705.006    B/op
[info] SoMemory.b:·gc.churn.PS_Survivor_Space       thrpt   20          20.838 ±        1.063  MB/sec
[info] SoMemory.b:·gc.churn.PS_Survivor_Space.norm  thrpt   20     8015328.037 ±   236873.550    B/op
[info] SoMemory.b:·gc.count                         thrpt   20         234.000                 counts
[info] SoMemory.b:·gc.time                          thrpt   20       37696.000                     ms

Note how smaller string has significantly higher gc.alloc.rate

SoMemory.a:·gc.alloc.rate         thrpt   20        3870.364 ±      120.687  MB/sec
SoMemory.b:·gc.alloc.rate         thrpt   20        2763.961 ±       89.654  MB/sec

Why there seems to be higher memory consumption in the first case when smaller string should have smaller memory footprint, for example, JOL gives for

class ZarA { val x = List.fill[String](1_000_000)(Random.nextString(1)) }
class ZarB { val x = List.fill[String](1_000_000)(Random.nextString(8)) }

as expected smaller footprint of approx 72MB for ZarA

example.ZarA@15975490d footprint:
     COUNT       AVG       SUM   DESCRIPTION
   1000000        24  24000000   [C
         1        16        16   example.ZarA
   1000000        24  24000000   java.lang.String
   1000000        24  24000000   scala.collection.immutable.$colon$colon
         1        16        16   scala.collection.immutable.Nil$
   3000002            72000032   (total)

compared to larger footprint of approx 80MB for ZarB

example.ZarB@15975490d footprint:
     COUNT       AVG       SUM   DESCRIPTION
   1000000        32  32000000   [C
         1        16        16   example.ZarB
   1000000        24  24000000   java.lang.String
   1000000        24  24000000   scala.collection.immutable.$colon$colon
         1        16        16   scala.collection.immutable.Nil$
   3000002            80000032   (total)

VisualVM memory behaviour

ZarA - used heap 129 MB

enter image description here

ZarB - used heap 91 MB

enter image description here


Solution

  • Allocation rate is how fast you can allocate memory (amount of memory allocated per unit of time). It doesn't tell us anything about total memory allocated.

    It is always easier to find smaller continuous memory region that larger contiguous memory region, so e.g. allocating e.g. 1000 Strings of length 1 should take impropotionaly less time than allocating e.g. 1000 String of length 8, resulting in higher allocation rate with less total memory consumption.