Java has EnumMap
- special Map
implementation designed for the case when keys are of enum
type (Please, see class declaration EnumMap<K extends Enum<K>,V>
). The main advantage over, for instance, java.util.HashMap
is the memory and performance efficiency, because it knows number of keys in advance, because they declared in enumeration, so it is memory compact and faster, because there is no need to extend internal table and resolve hash conflicts.
I'm wondering is there any Scala implementation out of the box for this Java implementation, but with Scala's Enumeration
and immutable.Map
interface. Maybe any external lib implementation or any way to create own implementation with close memory and performance results?
Thank you so much for a help in advance!
Enumeratum's open issue EnumSet/EnumMap #113 gives an informative benchmark
[info] MapComparisons.enumeratumScalaMapGet avgt 30 9.830 ± 0.207 ns/op
[info] MapComparisons.jEnumEnumMapGet avgt 30 4.745 ± 0.685 ns/op
[info] MapComparisons.jEnumScalaMapGet avgt 30 12.204 ± 0.186 ns/op
where lloydmeta states
So the current state of things is that Java EnumMap and EnumSet are roughly 2x faster than plain old Scala Set and Maps w/ Enumeratum, but being that we're still in the sub-10ns range, it seems reasonable to say that performance here won't be a bottleneck except in super edge cases.
I tried to replicate lloydmeta's benchmarks and got the following results with Scala 2.13, enumeratum 1.5.15
sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 -prof gc bench.So60374058"
Average time, time/op
enumeratumScalaMapGet avgt 20 8.284 ± 0.071 ns/op
jEnumEnumMapGet avgt 20 2.883 ± 0.023 ns/op
jEnumScalaMapGet avgt 20 7.361 ± 0.273 ns/op
vanillaScalaMapGet avgt 20 6.650 ± 0.323 ns/op
Memory allocation rate
enumeratumScalaMapGet:·gc.alloc.rate avgt 20 1753.262 ± 14.548 MB/sec
jEnumEnumMapGet:·gc.alloc.rate avgt 20 ≈ 10⁻⁴ MB/sec
jEnumScalaMapGet:·gc.alloc.rate avgt 20 1976.491 ± 70.259 MB/sec
vanillaScalaMapGet:·gc.alloc.rate avgt 20 2190.644 ± 105.208 MB/sec
We note jEnumScalaMapGet
seems to have caught up with enumeratumScalaMapGet
since 2017, and the trivial memory allocation of jEnumEnumMapGet
.
Benchmark source:
public enum JAgeGroup {
Baby,
Toddler,
Teenager,
Adult,
Senior
}
and
import java.util
import java.util.concurrent.TimeUnit
import example.JAgeGroup
import org.openjdk.jmh.annotations._
import org.openjdk.jmh.infra.Blackhole
import enumeratum._
object AgeGroupVanilla extends Enumeration {
type AgeGroupVanilla = Value
val Baby, Toddler, Teenager, Adult, Senior = Value
}
sealed trait AgeGroup extends EnumEntry
case object AgeGroup extends Enum[AgeGroup] {
val values = findValues
case object Baby extends AgeGroup
case object Toddler extends AgeGroup
case object Teenager extends AgeGroup
case object Adult extends AgeGroup
case object Senior extends AgeGroup
}
@State(Scope.Thread)
@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.NANOSECONDS)
class So60374058 {
private val jEnumEnumMap = {
val m: util.EnumMap[JAgeGroup, String] = new util.EnumMap(classOf[JAgeGroup])
JAgeGroup.values().foreach(e => m.put(e, e.name()))
m
}
private val jEnumScalaMap = Map(JAgeGroup.values().map(e => e -> e.name()): _*)
private val ageGroupScalaMap = Map(AgeGroup.values.map(e => e -> e.entryName): _*)
private val vanillaScalaMap = AgeGroupVanilla.values.map(e => e -> e.toString).toMap
private def randomFrom[A](seq: Seq[A]): A = {
seq(scala.util.Random.nextInt(seq.size))
}
private var jEnum: JAgeGroup = _
private var ageGroupEnum: AgeGroup = _
private var vanillaEnum: AgeGroupVanilla.AgeGroupVanilla = _
@Setup(Level.Trial)
def setup(): Unit = {
jEnum = randomFrom(JAgeGroup.values())
ageGroupEnum = randomFrom(AgeGroup.values)
vanillaEnum = randomFrom(AgeGroupVanilla.values.toSeq)
}
@Benchmark
def jEnumEnumMapGet(bh: Blackhole): Unit = bh.consume {
jEnumEnumMap.get(jEnum)
}
@Benchmark
def jEnumScalaMapGet(bh: Blackhole): Unit = bh.consume {
jEnumScalaMap.get(jEnum)
}
@Benchmark
def enumeratumScalaMapGet(bh: Blackhole): Unit = bh.consume {
ageGroupScalaMap.get(ageGroupEnum)
}
@Benchmark
def vanillaScalaMapGet(bh: Blackhole): Unit = bh.consume {
vanillaScalaMap.get(vanillaEnum)
}
}