Skip to content

Commit 302cb2a

Browse files
committed
Add AnnotationsMappingBenchmark
1 parent 2c1fe92 commit 302cb2a

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed

bench-micro/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
results

bench-micro/scripts/run.sh

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env bash
2+
3+
name=$1
4+
runs=2
5+
6+
run_benchmark() {
7+
jvm=$1
8+
jvm_coursier_id=$2
9+
run=$3
10+
json_file=results/$name-$jvm-$run.json
11+
txt_file=results/$name-$jvm-$run.txt
12+
13+
eval "$(coursier java --jvm "$jvm_coursier_id" --env)"
14+
rm -rf "$json_file" "$stdout_file" .bloop .sbt .bsp .metals target
15+
sbt "clean; scala3-bench-micro / Jmh / run -rf JSON -rff $json_file -o $txt_file $name"
16+
}
17+
18+
for run in $(seq 1 $runs); do
19+
run_benchmark openjdk "adoptium:1.17.0.11" $run
20+
run_benchmark graal "graalvm-java17:22.3.3" $run
21+
done
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package dotty.tools.benchmarks
2+
3+
import org.openjdk.jmh.annotations.{Benchmark, BenchmarkMode, Fork, Level, Measurement, Mode as JMHMode, Param, Scope, Setup, State, Warmup}
4+
import java.util.concurrent.TimeUnit.SECONDS
5+
6+
import dotty.tools.dotc.{Driver, Run, Compiler}
7+
import dotty.tools.dotc.ast.{tpd, TreeTypeMap}, tpd.{Apply, Block, Tree, TreeAccumulator, TypeApply}
8+
import dotty.tools.dotc.core.Annotations.{Annotation, ConcreteAnnotation, EmptyAnnotation}
9+
import dotty.tools.dotc.core.Contexts.{ContextBase, Context, ctx, withMode}
10+
import dotty.tools.dotc.core.Mode
11+
import dotty.tools.dotc.core.Phases.Phase
12+
import dotty.tools.dotc.core.Symbols.{defn, mapSymbols, Symbol}
13+
import dotty.tools.dotc.core.Types.{AnnotatedType, NoType, SkolemType, TermRef, Type, TypeMap}
14+
import dotty.tools.dotc.parsing.Parser
15+
import dotty.tools.dotc.typer.TyperPhase
16+
17+
/** Benchmark to compare different ways to map concrete annotations.
18+
*
19+
* The main performance bottleneck there is the mapping of the annotation's
20+
* inner tree of; a `TreeTypeMap` is much slower than a `TypeMap`.
21+
*
22+
* Run with: scala3-bench-micro / Jmh / run AnnotationsMappingBenchmark
23+
*/
24+
@Fork(value = 5)
25+
// Set to 0 to record all iterations. We remove the first iterations manually
26+
// when processing the results.
27+
@Warmup(iterations = 0, time = 1, timeUnit = SECONDS)
28+
@Measurement(iterations = 10, time = 1, timeUnit = SECONDS)
29+
@BenchmarkMode(Array(JMHMode.Throughput))
30+
@State(Scope.Thread)
31+
class AnnotationsMappingBenchmark:
32+
var tp: Type = null
33+
var specialIntTp: Type = null
34+
var context: Context = null
35+
var typeFunction: Context ?=> Type => Type = null
36+
var typeMap: TypeMap = null
37+
38+
@Param(Array("v1", "v2", "v3", "v4"))
39+
var valName: String = null
40+
41+
@Param(Array("current", "oldCheck", "newCheckEquals", "newCheckEq", "noCheck", "noCheckCopySymbols"))
42+
var typeMapName: String = null
43+
44+
@Param(Array("id", "mapInts"))
45+
var typeFunctionName: String = null
46+
47+
@Setup(Level.Iteration)
48+
def setup(): Unit =
49+
/** A custom phase that is used to retrieve the `Type`s and `Context` to be
50+
* used in the benchmark.
51+
*/
52+
val testPhase =
53+
new Phase:
54+
final override def phaseName = "testPhase"
55+
final override def run(using ctx: Context): Unit =
56+
val pkg = ctx.compilationUnit.tpdTree.symbol
57+
tp = pkg.requiredClass("Test").requiredValueRef(valName).underlying
58+
specialIntTp = pkg.requiredClass("Test").requiredType("SpecialInt").typeRef
59+
context = ctx
60+
61+
/** A custom compiler that only runs the `Parser`, `TyperPhase` and
62+
* `testPhase`.
63+
*/
64+
val compiler =
65+
new Compiler:
66+
private final val baseCompiler = new Compiler()
67+
final override def phases = List(List(Parser()), List(TyperPhase()), List(testPhase))
68+
69+
/** A custom driver that uses `compiler`. */
70+
val driver =
71+
new Driver:
72+
final override def newCompiler(using Context): Compiler = compiler
73+
74+
// Runs the driver with the test file.
75+
driver.process(Array("-classpath", System.getProperty("BENCH_CLASS_PATH"), "tests/someAnnotatedTypes.scala"))
76+
77+
typeFunction =
78+
typeFunctionName match
79+
case "id" => tp => tp
80+
case "mapInts" => tp => (if tp frozen_=:= defn.IntType then specialIntTp else tp)
81+
case _ => throw new IllegalArgumentException(s"Unknown type function: $typeFunctionName")
82+
83+
/** Creates a new `TypeMap` that uses `mapConcreteAnnotationWith` to map
84+
* concrete annotations. It is used to compare several ways to map these
85+
* annotations.
86+
*/
87+
def makeTypeMap(mapConcreteAnnotationWith: (ConcreteAnnotation, TypeMap) => Context ?=> Annotation) =
88+
new TypeMap(using context):
89+
final override def apply(tp: Type): Type = typeFunction(mapOver(tp))
90+
final override def mapOver(tp: Type) =
91+
tp match
92+
case tp @ AnnotatedType(underlying, annot) =>
93+
val underlying1 = this(underlying)
94+
val annot1 =
95+
annot match
96+
case annot: ConcreteAnnotation => mapConcreteAnnotationWith(annot, this)
97+
case _ => annot.mapWith(this)
98+
if annot1 eq EmptyAnnotation then underlying1
99+
else derivedAnnotatedType(tp, underlying1, annot1)
100+
case _ => super.mapOver(tp)
101+
102+
/** Retrieves all argument from a tree. This old implementation does not
103+
* include type arguments.
104+
*/
105+
def oldAllArguments(tree: Tree)(using Context): List[Tree] =
106+
tpd.unsplice(tree) match
107+
case Apply(fn, args) => oldAllArguments(fn) ::: args
108+
case TypeApply(fn, _) => oldAllArguments(fn)
109+
case Block(_, expr) => oldAllArguments(expr)
110+
case _ => Nil
111+
112+
/** This is the old (<= d1489734b7) implementation of `Annotation.mapWith`.
113+
* It 1. does not include type arguments and 2. uses `frozen_=:=` to
114+
* compare types and 3. does not copy all symbols.
115+
*/
116+
def oldMapWith(annot: ConcreteAnnotation, tm: TypeMap)(using Context): Annotation =
117+
val tree = annot.tree
118+
val args = oldAllArguments(tree)
119+
if args.isEmpty then annot
120+
else
121+
val findDiff = new TreeAccumulator[Type]:
122+
def apply(x: Type, tree: Tree)(using Context): Type =
123+
if tm.isRange(x) then x
124+
else
125+
val tp1 = tm(tree.tpe)
126+
foldOver(if tp1 frozen_=:= tree.tpe then x else tp1, tree)
127+
val diff = findDiff(NoType, args)
128+
if tm.isRange(diff) then EmptyAnnotation
129+
else if diff.exists then annot.derivedAnnotation(tm.mapOver(tree))
130+
else annot
131+
132+
/** Retrieves all argument from a tree, including type arguments. */
133+
def newAllArguments(tree: Tree)(using Context): List[Tree] =
134+
tpd.unsplice(tree) match
135+
case Apply(fn, args) => newAllArguments(fn) ::: args
136+
case TypeApply(fn, args) => newAllArguments(fn) ::: args
137+
case Block(_, expr) => newAllArguments(expr)
138+
case _ => Nil
139+
140+
/** This is the new implementation of `Annotation.mapWith`. It 1. includes
141+
* type arguments and 2. uses `==` to compare types and 3. copies all
142+
* symbols by using a custom `TreeTypeMap` that overrides `withMappedSyms`.
143+
*/
144+
def newMapWithEquals(annot: ConcreteAnnotation, tm: TypeMap)(using Context): Annotation =
145+
val tree = annot.tree
146+
val args = newAllArguments(tree)
147+
if args.isEmpty then annot
148+
else
149+
val findDiff = new TreeAccumulator[Type]:
150+
def apply(x: Type, tree: Tree)(using Context): Type =
151+
if tm.isRange(x) then x
152+
else
153+
val tp1 = tm(tree.tpe)
154+
foldOver(if tp1 == tree.tpe then x else tp1, tree)
155+
val diff = findDiff(NoType, args)
156+
if tm.isRange(diff) then EmptyAnnotation
157+
else if diff.exists then
158+
val ttm =
159+
new TreeTypeMap(tm):
160+
final override def withMappedSyms(syms: List[Symbol]): TreeTypeMap =
161+
withMappedSyms(syms, mapSymbols(syms, this, mapAlways = true))
162+
annot.derivedAnnotation(ttm.transform(tree))
163+
else annot
164+
165+
/** Exactly the same as `newMapWithEquals`, but uses `eq` instead of `==` to
166+
* compare types.
167+
*/
168+
def newMapWithEq(annot: ConcreteAnnotation, tm: TypeMap)(using Context): Annotation =
169+
val tree = annot.tree
170+
val args = newAllArguments(tree)
171+
if args.isEmpty then annot
172+
else
173+
val findDiff = new TreeAccumulator[Type]:
174+
def apply(x: Type, tree: Tree)(using Context): Type =
175+
if tm.isRange(x) then x
176+
else
177+
val tp1 = tm(tree.tpe)
178+
foldOver(if tp1 eq tree.tpe then x else tp1, tree)
179+
val diff = findDiff(NoType, args)
180+
if tm.isRange(diff) then EmptyAnnotation
181+
else if diff.exists then
182+
val ttm =
183+
new TreeTypeMap(tm):
184+
final override def withMappedSyms(syms: List[Symbol]): TreeTypeMap =
185+
withMappedSyms(syms, mapSymbols(syms, this, mapAlways = true))
186+
annot.derivedAnnotation(ttm.transform(tree))
187+
else annot
188+
189+
def noCheckMapWith(annot: ConcreteAnnotation, tm: TypeMap)(using Context): Annotation =
190+
annot.derivedAnnotation(tm.mapOver(annot.tree))
191+
192+
def noCheckCopySymbolsMapWith(annot: ConcreteAnnotation, tm: TypeMap)(using Context): Annotation =
193+
val ttm =
194+
new TreeTypeMap(tm):
195+
final override def withMappedSyms(syms: List[Symbol]): TreeTypeMap =
196+
withMappedSyms(syms, mapSymbols(syms, this, mapAlways = true))
197+
annot.derivedAnnotation(ttm.transform(annot.tree))
198+
199+
typeMap =
200+
typeMapName match
201+
case "current" =>
202+
new TypeMap(using context):
203+
final override def apply(tp: Type): Type = typeFunction(mapOver(tp))
204+
case "oldCheck" =>
205+
makeTypeMap(oldMapWith)
206+
case "newCheckEquals" =>
207+
// This should be the same as `current`, modulo a few indirections.
208+
makeTypeMap(newMapWithEq)
209+
case "newCheckEq" =>
210+
makeTypeMap(newMapWithEq)
211+
case "noCheck" =>
212+
makeTypeMap(noCheckMapWith)
213+
case "noCheckCopySymbols" =>
214+
makeTypeMap(noCheckCopySymbolsMapWith)
215+
case _ =>
216+
throw new IllegalArgumentException(s"Unknown type map: $typeMapName")
217+
218+
@Benchmark
219+
def applyTypeMap() =
220+
val res = typeMap.apply(tp)
221+
// println(res.show(using context))
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class Test:
2+
class FlagAnnot extends annotation.StaticAnnotation
3+
class StringAnnot(val s: String) extends annotation.StaticAnnotation
4+
class LambdaAnnot(val f: Int => Boolean) extends annotation.StaticAnnotation
5+
6+
type SpecialInt <: Int
7+
8+
val v1: Int @FlagAnnot = 42
9+
10+
val v2: Int @StringAnnot("hello") = 42
11+
12+
val v3: Int @LambdaAnnot(it => it == 42) = 42
13+
14+
val v4: Int @LambdaAnnot(it => {
15+
def g(x: Int, y: Int) = x - y + 5
16+
g(it, 7) * 2 == 80
17+
}) = 42
18+
19+
/*val v5: Int @LambdaAnnot(it => {
20+
class Foo(x: Int):
21+
def xPlus10 = x + 10
22+
def xPlus20 = x + 20
23+
def xPlus(y: Int) = x + y
24+
val foo = Foo(it)
25+
foo.xPlus10 - foo.xPlus20 + foo.xPlus(30) == 62
26+
}) = 42*/
27+
28+
def main(args: Array[String]): Unit = ???

0 commit comments

Comments
 (0)