Skip to content

Commit 0a3de65

Browse files
authored
Scala 3 perf improvements, consolidate rep implementations (#273)
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 ```
1 parent 22051f0 commit 0a3de65

File tree

13 files changed

+324
-187
lines changed

13 files changed

+324
-187
lines changed

build.sc

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import de.tobiasroeser.mill.vcs.version.VcsVersion
1313
import $ivy.`com.github.lolgab::mill-mima::0.0.13`
1414
import com.github.lolgab.mill.mima._
1515

16-
val scala31 = "3.1.3"
16+
val scala31 = "3.2.2"
1717
val scala213 = "2.13.10"
1818
val scala212 = "2.12.17"
1919
val scala211 = "2.11.12"
@@ -237,16 +237,17 @@ object perftests extends Module{
237237
object bench2 extends PerfTestModule {
238238
def scalaVersion0 = scala213
239239
def moduleDeps = Seq(
240-
scalaparse.jvm(scala212).test,
241-
pythonparse.jvm(scala212).test,
242-
cssparse.jvm(scala212).test,
243-
fastparse.jvm(scala212).test,
240+
scalaparse.jvm(scala213).test,
241+
pythonparse.jvm(scala213).test,
242+
cssparse.jvm(scala213).test,
243+
fastparse.jvm(scala213).test,
244244
)
245245

246246
}
247247

248248
object benchScala3 extends PerfTestModule {
249249
def scalaVersion0 = scala31
250+
def sources = T.sources{ bench2.sources() }
250251
def moduleDeps = Seq(
251252
scalaparse.jvm(scala31).test,
252253
pythonparse.jvm(scala31).test,

fastparse/src-2/fastparse/internal/MacroImpls.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ object MacroImpls {
5454
if (ctx0.verboseFailures) {
5555
ctx0.aggregateMsg(
5656
startIndex,
57-
Msgs(List(new Lazy(() => name.splice.value))),
57+
Msgs(new Lazy(() => name.splice.value) :: Nil),
5858
ctx0.failureGroupAggregate,
5959
startIndex < ctx0.traceIndex
6060
)

fastparse/src/fastparse/internal/RepImpls.scala renamed to fastparse/src-2/fastparse/internal/RepImpls.scala

Lines changed: 33 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package fastparse.internal
22

33

44
import fastparse.{Implicits, NoWhitespace, ParsingRun}
5-
5+
import Util.{aggregateMsgInRep, aggregateMsgPostSep}
66
import scala.annotation.tailrec
77

8+
89
class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
910
def repX[V](min: Int = 0,
1011
sep: => ParsingRun[_] = null,
@@ -148,7 +149,9 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
148149
outerCut: Boolean,
149150
sepMsg: Msgs,
150151
lastAgg: Msgs): ParsingRun[V] = {
152+
151153
ctx.cut = precut | (count < min && outerCut)
154+
152155
if (count == 0 && actualMax == 0) ctx.freshSuccess(repeater.result(acc), startIndex)
153156
else {
154157
parse0()
@@ -171,36 +174,34 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
171174
if (verboseFailures) ctx.setMsg(startIndex, () => parsedMsg.render + ".rep" + (if(min == 0) "" else s"($min)"))
172175
res
173176
}
177+
else if (!consumeWhitespace(whitespace, ctx, false)) ctx.asInstanceOf[ParsingRun[Nothing]]
174178
else {
175-
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)
176-
177-
if (!ctx.isSuccess && ctx.cut) ctx.asInstanceOf[ParsingRun[Nothing]]
178-
else {
179-
ctx.cut = false
180-
val sep1 = sep
181-
val sepCut = ctx.cut
182-
val endCut = outerCut | postCut | sepCut
183-
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
184-
else if (ctx.isSuccess) {
185-
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)
186-
if (!ctx.isSuccess && sepCut) ctx.asInstanceOf[ParsingRun[Nothing]]
187-
else rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
188-
}
179+
ctx.cut = false
180+
val sep1 = sep
181+
val sepCut = ctx.cut
182+
val endCut = outerCut | postCut | sepCut
183+
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
184+
else if (ctx.isSuccess) {
185+
if (!consumeWhitespace(whitespace, ctx, sepCut)) ctx.asInstanceOf[ParsingRun[Nothing]]
189186
else {
190-
val res =
191-
if (sepCut) ctx.augmentFailure(beforeSepIndex, endCut)
192-
else end(beforeSepIndex, beforeSepIndex, nextCount, endCut)
193-
194-
if (verboseFailures) aggregateMsgPostSep(startIndex, min, ctx, parsedMsg, parsedAgg)
195-
res
187+
rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
196188
}
197189
}
190+
else {
191+
val res =
192+
if (sepCut) ctx.augmentFailure(beforeSepIndex, endCut)
193+
else end(beforeSepIndex, beforeSepIndex, nextCount, endCut)
194+
195+
if (verboseFailures) aggregateMsgPostSep(startIndex, min, ctx, parsedMsg, parsedAgg)
196+
res
197+
}
198198
}
199199
}
200200
}
201201
}
202202
rec(ctx.index, 0, false, ctx.cut, null, null)
203203
}
204+
204205
def rep[V](min: Int,
205206
sep: => ParsingRun[_])
206207
(implicit repeater: Implicits.Repeater[T, V],
@@ -236,19 +237,18 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
236237
val beforeSepIndex = ctx.index
237238
repeater.accumulate(ctx.successValue.asInstanceOf[T], acc)
238239
val nextCount = count + 1
239-
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)
240-
241-
if (!ctx.isSuccess && ctx.cut) ctx.asInstanceOf[ParsingRun[Nothing]]
240+
if (!consumeWhitespace(whitespace, ctx, false)) ctx.asInstanceOf[ParsingRun[Nothing]]
242241
else {
243242
ctx.cut = false
244243
val sep1 = sep
245244
val sepCut = ctx.cut
246245
val endCut = outerCut | postCut | sepCut
247246
if (sep1 == null) rec(beforeSepIndex, nextCount, false, endCut, null, parsedAgg)
248247
else if (ctx.isSuccess) {
249-
if (whitespace ne NoWhitespace.noWhitespaceImplicit) Util.consumeWhitespace(whitespace, ctx)
250-
251-
rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
248+
if (!consumeWhitespace(whitespace, ctx, sepCut)) ctx.asInstanceOf[ParsingRun[Nothing]]
249+
else {
250+
rec(beforeSepIndex, nextCount, sepCut, endCut, ctx.shortParserMsg, parsedAgg)
251+
}
252252
}
253253
else {
254254
val res =
@@ -264,49 +264,12 @@ class RepImpls[T](val parse0: () => ParsingRun[T]) extends AnyVal{
264264
rec(ctx.index, 0, false, ctx.cut, null, null)
265265
}
266266

267-
private def aggregateMsgPostSep[V](startIndex: Int,
268-
min: Int,
269-
ctx: ParsingRun[Any],
270-
parsedMsg: Msgs,
271-
lastAgg: Msgs) = {
272-
ctx.aggregateMsg(
273-
startIndex,
274-
() => parsedMsg.render + s".rep($min)",
275-
// When we fail on a sep, we collect the failure aggregate of the last
276-
// non-sep rep body together with the failure aggregate of the sep, since
277-
// the last non-sep rep body continuing is one of the valid ways of
278-
// continuing the parse
279-
ctx.failureGroupAggregate ::: lastAgg
280-
281-
)
282-
}
283-
284-
private def aggregateMsgInRep[V](startIndex: Int,
285-
min: Int,
286-
ctx: ParsingRun[Any],
287-
sepMsg: Msgs,
288-
parsedMsg: Msgs,
289-
lastAgg: Msgs,
290-
precut: Boolean) = {
291-
if (sepMsg == null || precut) {
292-
ctx.aggregateMsg(
293-
startIndex,
294-
() => parsedMsg.render + s".rep($min)",
295-
if (lastAgg == null) ctx.failureGroupAggregate
296-
else ctx.failureGroupAggregate ::: lastAgg
297-
)
298-
} else {
299-
ctx.aggregateMsg(
300-
startIndex,
301-
() => parsedMsg.render + s".rep($min)",
302-
// When we fail on a rep body, we collect both the concatenated
303-
// sep and failure aggregate of the rep body that we tried (because
304-
// we backtrack past the sep on failure) as well as the failure
305-
// aggregate of the previous rep, which we could have continued
306-
if (lastAgg == null) Util.joinBinOp(sepMsg, parsedMsg)
307-
else Util.joinBinOp(sepMsg, parsedMsg) ::: lastAgg
308-
)
267+
private def consumeWhitespace(whitespace: fastparse.Whitespace, ctx: ParsingRun[_], extraCut: Boolean) = {
268+
if (whitespace eq NoWhitespace.noWhitespaceImplicit) true
269+
else {
270+
Util.consumeWhitespace(whitespace, ctx)
271+
if (!ctx.isSuccess && (extraCut || ctx.cut)) false
272+
else true
309273
}
310274
}
311-
312275
}

fastparse/src-3/fastparse/internal/MacroInlineImpls.scala

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,18 @@ object MacroInlineImpls {
3434
}
3535
} else {
3636
val xLength = Expr[Int](x.length)
37-
val checker =
38-
'{ (string: _root_.fastparse.ParserInput, offset: _root_.scala.Int) =>
39-
${
40-
x.zipWithIndex
41-
.map { case (char, i) => '{ string.apply(offset + ${ Expr(i) }) == ${ Expr(char) } } }
42-
.reduce[Expr[Boolean]] { case (l, r) => '{ $l && $r } }
43-
}
44-
}
4537
'{
46-
4738
$ctx match {
4839
case ctx1 =>
4940
val index = ctx1.index
5041
val end = index + $xLength
5142
val input = ctx1.input
5243
val res =
53-
if (input.isReachable(end - 1) && ${ checker }(input, index)) {
44+
if (input.isReachable(end - 1) && ${
45+
x.zipWithIndex
46+
.map { case (char, i) => '{ input.apply(index + ${ Expr(i) }) == ${ Expr(char) } } }
47+
.reduce[Expr[Boolean]] { case (l, r) => '{ $l && $r } }
48+
}) {
5449
ctx1.freshSuccessUnit(end)
5550
} else {
5651
ctx1.freshFailure().asInstanceOf[ParsingRun[Unit]]
@@ -99,17 +94,18 @@ object MacroInlineImpls {
9994

10095
val startIndex = ctx1.index
10196
val instrument = ctx1.instrument != null
102-
val ctx0 = t
10397
if (instrument) {
10498
ctx1.instrument.beforeParse(name.value, startIndex)
10599
}
100+
val ctx0 = t
101+
106102
if (instrument) {
107103
ctx1.instrument.afterParse(name.value, ctx0.index, ctx0.isSuccess)
108104
}
109105
if (ctx0.verboseFailures) {
110106
ctx0.aggregateMsg(
111107
startIndex,
112-
Msgs(List(new Lazy(() => name.value))),
108+
Msgs(new Lazy(() => name.value) :: Nil),
113109
ctx0.failureGroupAggregate,
114110
startIndex < ctx0.traceIndex
115111
)

0 commit comments

Comments
 (0)