0xDEADBEEF

[RSS]
««« »»»

Java, Scala a regulární výrazy #2 - rychlost

27. 11. 2017

Stará poučka říká, že v Javě bychom měli regulární výrazy vždy dopředu zkompilovat a pak je opakovaně používat, protože kompilování je časově náročné.

V Javě je na to volání Patten.compile("^===+$"). Ve Scale je možné použít kompaktnější zápis za pomocí kouzel implicitních metod "^===+$".r.

Ale jak pomalé to vlastně je? To se dá jednoduše zjistit.

Vytáhl jsem JMH, napsal benchmark, který hledá jednoduchý vzor. Jednou vzor nekompiluje, podruhé používá předkompilovaný vzor a potřetí hledá regex jen když je to nezbytně nutné.

@State(Scope.Thread)
class regex {

  var lines: Array[String] = _
  val regex = """^===+$""".r

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

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

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

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

Jako testovací data jsem použil kompletní export zdrojových textů blogu k47.cz, které mají dohromady něco kolem 8 MB a 143000 řádek textu.

Výsledky jsou následující:

Benchmark           Mode   Cnt    Score    Error  Units
notCompiled        thrpt     6   28,936 ±  2,298  ops/s
compiled           thrpt     6   94,120 ±  1,195  ops/s
compiledWithCheck  thrpt     6  526,849 ± 96,101  ops/s

Kompilovaný regex je tři-a-něco-krát rychlejší než nekompilovaný. Ale ten zaostává za případem, kdy se vyhodnocování regexu úplně vyhnu. Přitom regex by měl selhat hned potom, co přečte první znak a tedy by neměl dělat víc práce, než explicitní kontrola. Ale očividně má nezanedbatelnou režii.

Pokud regex nebude často prováděn, je rychlejší dělat něco jako:

// zkompilovaný regex
val linkRegex = """(?x) " ([^"]+?) " : \[ ([^\]\n]+?) \]""".r
val linkCheck = "\":["

// používat ho následovně
if (txt.contains(linkCheck)) {
  linkRegex.replaceAllIn(txt, m => makeLink(m))
}

Narychlo zkontrolovat, jestli zdrojový řetězec obsahuje nějakou fixní část regexu a teprve potom nechat regex dělat těžkou práci. Ale jako v případě každé optimalizace je třeba profilovat a benchmarkovat.

Tento přístup zrychlil renderování textu asciiblogu o 75%.


Relevantní čtení:

píše k47 (@kaja47, k47)