Java, Scala a regulární výrazy #6 - znovupoužití Matcher objektu
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ů:
- nekompilované regexy
- kompilované regexy
- kompilované regexy, které recyklují
Matcher
- kompilované regexy s rychlou kontrolou konstantního stringu, která se snaží obejít vykonání regexu pokud je to možné
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 Matcher
u je 7×.
Jako obvykle je nejrychlejší regexy vůbec neprovádět.
Dodatek: S narůstající složitostí regexů, zdá se, má znovoupoužívání Matcher objetu menší význam. Pro regexy na parsování apache logů, přinesou jen pár procent zrychlení.
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() } }