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
ZarB - used heap 91 MB
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.