0xDEADBEEF

[RSS]
««« »»»

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

26. 1. 2019

Mnoho API v Javě je na­vr­ženo s ohle­dem na zno­vupo­u­žití vy­tvo­ře­ných ob­jektů. Na­pří­klad XML­Stre­a­mWri­ter alo­kuje ne­za­ne­dba­telné množ­ství in­ter­ních struk­tur a proto je pod­statně rych­lejší, když je vy­tvo­řen jen jednou a pak po­u­ží­ván opa­ko­vaně.

Stejně tak je možné při práci s re­gu­lár­ními výrazy opě­tovně po­u­ží­vat objekt Matcher me­to­dou reset. Při každém vy­tvo­ření Matcher ob­jektu dojde k alo­ko­vání ně­ko­lika in­ter­ních polí včetně pole In­tHa­sh­Set ob­jektů a to po­cho­pi­telně stojí čas.

Na­místo kla­sic­kého

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

můžu použít va­ri­antu

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

Ta vy­tvoří pouze jeden Matcher objekt a opa­ko­vaně ho recykluje.

Abych zjis­til, jaký je mezi jed­not­li­vými pří­pady rozdíl, napsal jsem JMH ben­chmark, který tes­tuje ně­ko­lik 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á jed­no­du­chý regex, recyk­lace vede ke dvoj­ná­sobné rych­losti hle­dání. Rozdíl mezi na­iv­ním po­u­ži­tím regexů me­to­dou string.matches(regex) a zno­vupo­u­ži­tím Matcheru je 7x.

Jako ob­vykle je nej­rych­lejší regexy vůbec ne­pro­vá­dět.


Do­da­tek: S na­růs­ta­jící slo­ži­tostí regexů, zdá se, má zno­vou­pou­ží­vání Matcher objetu menší význam. Pro regexy na par­so­vání apache logů, při­ne­sou jen pár pro­cent zrych­lení.


Ben­chmark:

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