0xDEADBEEF

RSS
««« »»»

Java, Scala a regulární výrazy #6 - znovupoužití Matcher objektu

26. 1. 2019

Mnoho API v Javě je navrženo s ohledem na znovupoužití vytvořených objektů. Například XMLStreamWriter alokuje nezanedbatelné množství interních struktur a proto je podstatně rychlejší, když je vytvořen jen jednou a pak používán opakovaně.

Stejně tak je možné při práci s regulárními výrazy opětovně používat objekt Matcher metodou reset. Při každém vytvoření Matcher objektu dojde k alokování několika interních polí včetně pole IntHashSet objektů a to pochopitelně stojí čas.

Namísto klasického

val pattern = Pattern.compile("^===+$")
lines.count { line => pattern.matcher(line).matches() }

můžu použít variantu

val pattern = Pattern.compile("^===+$")
val matcher = pattern.matcher("")
lines.count { line => matcher.reset(line).matches() }

Ta vytvoří pouze jeden Matcher objekt a opakovaně ho recykluje.

Abych zjistil, jaký je mezi jednotlivými případy rozdíl, napsal jsem JMH benchmark, který testuje několik případů:

Benchmark                      Mode  Cnt    Score    Error  Units
a.r.notcompiled               thrpt    3   23,619 ±  2,003  ops/s
a.r.compiled_scala_match      thrpt    3   78,393 ±  6,214  ops/s
a.r.compiled_java             thrpt    3   80,988 ±  7,783  ops/s
a.r.reuse_matcher             thrpt    3  163,157 ± 11,151  ops/s
a.r.compiled_with_check       thrpt    3  382,765 ± 14,174  ops/s
a.r.reuse_matcher_with_check  thrpt    3  416,959 ± 17,088  ops/s

V tomto případě, kdy se hledá jednoduchý regex, recyklace vede ke dvojnásobné rychlosti hledání. Rozdíl mezi naivním použitím regexů metodou string.matches(regex) a znovupoužitím Matcheru je 7x.

Jako obvykle je nejrychlejší regexy vůbec neprovádět.


Benchmark:

@State(Scope.Thread)
class regex {
  var lines: Array[String] = _
  val regex: util.matching.Regex = """^===+$""".r
  val pattern = regex.pattern
  val matcher = pattern.matcher("")

  @Setup
  def prepare() = {
    lines = io.Source.fromFile("k47merged").getLines.toArray
  }

  @Benchmark
  def notcompiled(): Int =
    lines.count(l => l.matches("^===+$"))

  @Benchmark
  def compiled_scala_match(): Int =
    lines.count {
      case regex() => true
      case _ => false
    }

  @Benchmark
  def compiled_java(): Int =
    lines.count { l => pattern.matcher(l).matches() }

  @Benchmark
  def reuse_matcher(): Int =
    lines.count { l => matcher.reset(l).matches() }

  @Benchmark
  def compiled_with_check(): Int =
    lines.count { l => (l.length >= 3 && l.charAt(0) == '=') && regex.findFirstMatchIn(l).nonEmpty }

  @Benchmark
  def reuse_matcher_with_check(): Int =
    lines.count { l => (l.length >= 3 && l.charAt(0) == '=') && matcher.reset(l).matches() }
}
píše k47 (@kaja47, k47)