Scala enumeration map implementation

Ivan Kurchenko :

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!

Mario Galic :

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)
  }
}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=3720&siteId=1