Skip to content

Commit

Permalink
Scala 3 perf improvements, consolidate rep implementations (#273)
Browse files Browse the repository at this point in the history
1. Previously `.rep()` never inlined, and `.repX()` would always inline
and fail with a compile error if it was not possible. This should make
both perform inlining at a best-effort level, giving us the best of both
worlds

2. I saw a lot of calls to `List()` turning up in my JProfile, replaced
them with raw `::` calls. Seems like this is optmized automatically in
Scala 2, but not in Scala 3
scala/scala3#17035

3. Lift whitespace NoWhitespace check in `.rep` to compile time, like we
do in `~`.

4. Combine all the menagerie of `.rep` implementations into one Scala 3
macro. Scala 2 is still a bit of a mess, cleaning that up is left for
future

6. Inlined the character checking in `literalStrMacro` to avoid
allocating an intermediate function to call

Seems to provide a small but measurable performance boost for successful
parses, and a significant performance boost for failures (where we
allocate a lot more lists as part of error reporting). Not quite enough
to catch up to Scala 2 performance, but brings us maybe half way there.

Scala 3 before:

```
JsonnetParse Benchmark
Max time - 10000 ms. Iterations - 5.
Iteration 1
Benchmark 0. Result: 9331
Benchmark 1. Result: 1546
Iteration 2
Benchmark 0. Result: 7715
Benchmark 1. Result: 1947
Iteration 3
Benchmark 0. Result: 7549
Benchmark 1. Result: 1976
Iteration 4
Benchmark 0. Result: 7613
Benchmark 1. Result: 1953
Iteration 5
Benchmark 0. Result: 7686
Benchmark 1. Result: 1907
```

Scala 3 after:

```
JsonnetParse Benchmark
Max time - 10000 ms. Iterations - 5.
Iteration 1
Benchmark 0. Result: 9466
Benchmark 1. Result: 2611
Iteration 2
Benchmark 0. Result: 8152
Benchmark 1. Result: 2832
Iteration 3
Benchmark 0. Result: 8139
Benchmark 1. Result: 2799
Iteration 4
Benchmark 0. Result: 8020
Benchmark 1. Result: 2844
Iteration 5
Benchmark 0. Result: 8126
Benchmark 1. Result: 2868
```

Scala 2.13:

```
JsonnetParse Benchmark
Max time - 10000 ms. Iterations - 5.
Iteration 1
Benchmark 0. Result: 9850
Benchmark 1. Result: 2773
Iteration 2
Benchmark 0. Result: 8781
Benchmark 1. Result: 2912
Iteration 3
Benchmark 0. Result: 8742
Benchmark 1. Result: 2916
Iteration 4
Benchmark 0. Result: 8782
Benchmark 1. Result: 2912
Iteration 5
Benchmark 0. Result: 8703
Benchmark 1. Result: 2940
```
  • Loading branch information
lihaoyi authored Mar 3, 2023
1 parent 22051f0 commit 0a3de65
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 187 deletions.
11 changes: 6 additions & 5 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import de.tobiasroeser.mill.vcs.version.VcsVersion
import $ivy.`com.github.lolgab::mill-mima::0.0.13`
import com.github.lolgab.mill.mima._

val scala31 = "3.1.3"
val scala31 = "3.2.2"
val scala213 = "2.13.10"
val scala212 = "2.12.17"
val scala211 = "2.11.12"
Expand Down Expand Up @@ -237,16 +237,17 @@ object perftests extends Module{
object bench2 extends PerfTestModule {
def scalaVersion0 = scala213
def moduleDeps = Seq(
scalaparse.jvm(scala212).test,
pythonparse.jvm(scala212).test,
cssparse.jvm(scala212).test,
fastparse.jvm(scala212).test,
scalaparse.jvm(scala213).test,
pythonparse.jvm(scala213).test,
cssparse.jvm(scala213).test,
fastparse.jvm(scala213).test,
)

}

object benchScala3 extends PerfTestModule {
def scalaVersion0 = scala31
def sources = T.sources{ bench2.sources() }
def moduleDeps = Seq(
scalaparse.jvm(scala31).test,
pythonparse.jvm(scala31).test,
Expand Down
2 changes: 1 addition & 1 deletion fastparse/src-2/fastparse/internal/MacroImpls.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object MacroImpls {
if (ctx0.verboseFailures) {
ctx0.aggregateMsg(
startIndex,
Msgs(List(new Lazy(() => name.splice.value))),
Msgs(new Lazy(() => name.splice.value) :: Nil),
ctx0.failureGroupAggregate,
startIndex < ctx0.traceIndex
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package fastparse.internal


import fastparse.{Implicits, NoWhitespace, ParsingRun}

import Util.{aggregateMsgInRep, aggregateMsgPostSep}
import scala.annotation.tailrec


class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
def repX[V](min: Int = 0,
sep: => ParsingRun[_] = null,
Expand Down Expand Up @@ -148,7 +149,9 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
outerCut: Boolean,
sepMsg: Msgs,
lastAgg: Msgs): ParsingRun[V] = {

ctx.cut = precut | (count < min && outerCut)

if (count == 0 && actualMax == 0) ctx.freshSuccess(repeater.result(acc), startIndex)
else {
parse0()
Expand All @@ -171,36 +174,34 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
if (verboseFailures) ctx.setMsg(startIndex, () => parsedMsg.render + ".rep" + (if(min == 0) "" else s"($min)"))
res
}
else if (!consumeWhitespace(whitespace, ctx, false)) ctx.asInstanceOf[ParsingRun[Nothing]]
else {
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)

if (!ctx.isSuccess && ctx.cut) ctx.asInstanceOf[ParsingRun[Nothing]]
else {
ctx.cut = false
val sep1 = sep
val sepCut = ctx.cut
val endCut = outerCut | postCut | sepCut
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
else if (ctx.isSuccess) {
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)
if (!ctx.isSuccess && sepCut) ctx.asInstanceOf[ParsingRun[Nothing]]
else rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
}
ctx.cut = false
val sep1 = sep
val sepCut = ctx.cut
val endCut = outerCut | postCut | sepCut
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
else if (ctx.isSuccess) {
if (!consumeWhitespace(whitespace, ctx, sepCut)) ctx.asInstanceOf[ParsingRun[Nothing]]
else {
val res =
if (sepCut) ctx.augmentFailure(beforeSepIndex, endCut)
else end(beforeSepIndex, beforeSepIndex, nextCount, endCut)

if (verboseFailures) aggregateMsgPostSep(startIndex, min, ctx, parsedMsg, parsedAgg)
res
rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
}
}
else {
val res =
if (sepCut) ctx.augmentFailure(beforeSepIndex, endCut)
else end(beforeSepIndex, beforeSepIndex, nextCount, endCut)

if (verboseFailures) aggregateMsgPostSep(startIndex, min, ctx, parsedMsg, parsedAgg)
res
}
}
}
}
}
rec(ctx.index, 0, false, ctx.cut, null, null)
}

def rep[V](min: Int,
sep: => ParsingRun[_])
(implicit repeater: Implicits.Repeater[T, V],
Expand Down Expand Up @@ -236,19 +237,18 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
val beforeSepIndex = ctx.index
repeater.accumulate(ctx.successValue.asInstanceOf[T], acc)
val nextCount = count + 1
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)

if (!ctx.isSuccess && ctx.cut) ctx.asInstanceOf[ParsingRun[Nothing]]
if (!consumeWhitespace(whitespace, ctx, false)) ctx.asInstanceOf[ParsingRun[Nothing]]
else {
ctx.cut = false
val sep1 = sep
val sepCut = ctx.cut
val endCut = outerCut | postCut | sepCut
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
else if (ctx.isSuccess) {
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)

rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
if (!consumeWhitespace(whitespace, ctx, sepCut)) ctx.asInstanceOf[ParsingRun[Nothing]]
else {
rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
}
}
else {
val res =
Expand All @@ -264,49 +264,12 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
rec(ctx.index, 0, false, ctx.cut, null, null)
}

private def aggregateMsgPostSep[V](startIndex: Int,
min: Int,
ctx: ParsingRun[Any],
parsedMsg: Msgs,
lastAgg: Msgs) = {
ctx.aggregateMsg(
startIndex,
() => parsedMsg.render + s".rep($min)",
// When we fail on a sep, we collect the failure aggregate of the last
// non-sep rep body together with the failure aggregate of the sep, since
// the last non-sep rep body continuing is one of the valid ways of
// continuing the parse
ctx.failureGroupAggregate ::: lastAgg

)
}

private def aggregateMsgInRep[V](startIndex: Int,
min: Int,
ctx: ParsingRun[Any],
sepMsg: Msgs,
parsedMsg: Msgs,
lastAgg: Msgs,
precut: Boolean) = {
if (sepMsg == null || precut) {
ctx.aggregateMsg(
startIndex,
() => parsedMsg.render + s".rep($min)",
if (lastAgg == null) ctx.failureGroupAggregate
else ctx.failureGroupAggregate ::: lastAgg
)
} else {
ctx.aggregateMsg(
startIndex,
() => parsedMsg.render + s".rep($min)",
// When we fail on a rep body, we collect both the concatenated
// sep and failure aggregate of the rep body that we tried (because
// we backtrack past the sep on failure) as well as the failure
// aggregate of the previous rep, which we could have continued
if (lastAgg == null) Util.joinBinOp(sepMsg, parsedMsg)
else Util.joinBinOp(sepMsg, parsedMsg) ::: lastAgg
)
private def consumeWhitespace(whitespace: fastparse.Whitespace, ctx: ParsingRun[_], extraCut: Boolean) = {
if (whitespace eq NoWhitespace.noWhitespaceImplicit) true
else {
Util.consumeWhitespace(whitespace, ctx)
if (!ctx.isSuccess && (extraCut || ctx.cut)) false
else true
}
}

}
20 changes: 8 additions & 12 deletions fastparse/src-3/fastparse/internal/MacroInlineImpls.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,18 @@ object MacroInlineImpls {
}
} else {
val xLength = Expr[Int](x.length)
val checker =
'{ (string: _root_.fastparse.ParserInput, offset: _root_.scala.Int) =>
${
x.zipWithIndex
.map { case (char, i) => '{ string.apply(offset + ${ Expr(i) }) == ${ Expr(char) } } }
.reduce[Expr[Boolean]] { case (l, r) => '{ $l && $r } }
}
}
'{

$ctx match {
case ctx1 =>
val index = ctx1.index
val end = index + $xLength
val input = ctx1.input
val res =
if (input.isReachable(end - 1) && ${ checker }(input, index)) {
if (input.isReachable(end - 1) && ${
x.zipWithIndex
.map { case (char, i) => '{ input.apply(index + ${ Expr(i) }) == ${ Expr(char) } } }
.reduce[Expr[Boolean]] { case (l, r) => '{ $l && $r } }
}) {
ctx1.freshSuccessUnit(end)
} else {
ctx1.freshFailure().asInstanceOf[ParsingRun[Unit]]
Expand Down Expand Up @@ -99,17 +94,18 @@ object MacroInlineImpls {

val startIndex = ctx1.index
val instrument = ctx1.instrument != null
val ctx0 = t
if (instrument) {
ctx1.instrument.beforeParse(name.value, startIndex)
}
val ctx0 = t

if (instrument) {
ctx1.instrument.afterParse(name.value, ctx0.index, ctx0.isSuccess)
}
if (ctx0.verboseFailures) {
ctx0.aggregateMsg(
startIndex,
Msgs(List(new Lazy(() => name.value))),
Msgs(new Lazy(() => name.value) :: Nil),
ctx0.failureGroupAggregate,
startIndex < ctx0.traceIndex
)
Expand Down
Loading

0 comments on commit 0a3de65

Please sign in to comment.