Skip to content

Commit 2654b56

Browse files
authored
Merge pull request #14273 from dotty-staging/mb/compiletime-ops-benchmarks
Add compiletime benchmarks and fix performance regression
2 parents 819c2ff + 0b5005d commit 2654b56

File tree

7 files changed

+229
-11
lines changed

7 files changed

+229
-11
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ tests/partest-generated/
5858
tests/locks/
5959
/test-classes/
6060

61+
# Benchmarks
62+
bench/tests-generated
63+
6164
# Ignore output files but keep the directory
6265
out/
6366
build/

bench/profiles/compiletime.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
charts:
2+
3+
- name: "Compile-time sums of constant integer types (generated)"
4+
url: https://github.com/lampepfl/dotty/blob/master/bench/src/main/scala/generateBenchmarks.scala
5+
lines:
6+
- key: compiletime-sum-constants
7+
label: bootstrapped
8+
9+
- name: "Compile-time sums of term reference types (generated)"
10+
url: https://github.com/lampepfl/dotty/blob/master/bench/src/main/scala/generateBenchmarks.scala
11+
lines:
12+
- key: compiletime-sum-termrefs
13+
label: bootstrapped
14+
15+
- name: "Sums of term references, result type inferred (generated)"
16+
url: https://github.com/lampepfl/dotty/blob/master/bench/src/main/scala/generateBenchmarks.scala
17+
lines:
18+
- key: compiletime-sum-termrefs-terms
19+
label: bootstrapped
20+
21+
- name: "Compile-time sums of type applications (generated)"
22+
url: https://github.com/lampepfl/dotty/blob/master/bench/src/main/scala/generateBenchmarks.scala
23+
lines:
24+
- key: compiletime-sum-applications
25+
label: bootstrapped
26+
27+
- name: "Compile-time additions inside multiplications (generated)"
28+
url: https://github.com/lampepfl/dotty/blob/master/bench/src/main/scala/generateBenchmarks.scala
29+
lines:
30+
- key: compiletime-distribute
31+
label: bootstrapped
32+
33+
scripts:
34+
35+
compiletime-sum-constants:
36+
- measure 6 6 7 1 $PROG_HOME/dotty/bench/tests-generated/compiletime-ops/sum-constants.scala
37+
38+
compiletime-sum-termrefs:
39+
- measure 6 6 7 1 $PROG_HOME/dotty/bench/tests-generated/compiletime-ops/sum-termrefs.scala
40+
41+
compiletime-sum-termrefs-terms:
42+
- measure 6 6 7 1 $PROG_HOME/dotty/bench/tests-generated/compiletime-ops/sum-termrefs-terms.scala
43+
44+
compiletime-sum-applications:
45+
- measure 6 6 7 1 $PROG_HOME/dotty/bench/tests-generated/compiletime-ops/sum-applications.scala
46+
47+
compiletime-distribute:
48+
- measure 6 6 7 1 $PROG_HOME/dotty/bench/tests-generated/compiletime-ops/distribute.scala
49+
50+
config:
51+
pr_base_url: "https://github.com/lampepfl/dotty/pull/"

bench/profiles/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ includes:
77
- empty.yml
88
- quotes.yml
99
- tuples.yml
10+
- compiletime.yml
1011

1112

1213
config:

bench/src/main/scala/Benchmarks.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import reporting._
88
import org.openjdk.jmh.results.RunResult
99
import org.openjdk.jmh.runner.Runner
1010
import org.openjdk.jmh.runner.options.OptionsBuilder
11+
import org.openjdk.jmh.runner.options.TimeValue
12+
//import org.openjdk.jmh.results.format.ResultFormatType
1113
import org.openjdk.jmh.annotations._
1214
import org.openjdk.jmh.results.format._
1315
import java.util.concurrent.TimeUnit
@@ -21,8 +23,11 @@ import dotty.tools.io.AbstractFile
2123

2224
object Bench {
2325
val COMPILE_OPTS_FILE = "compile.txt"
26+
val GENERATED_BENCHMARKS_DIR = "tests-generated"
2427

2528
def main(args: Array[String]): Unit = {
29+
generateBenchmarks(GENERATED_BENCHMARKS_DIR)
30+
2631
if (args.isEmpty) {
2732
println("Missing <args>")
2833
return
@@ -32,7 +37,7 @@ object Bench {
3237
val warmup = if (intArgs.length > 0) intArgs(0).toInt else 30
3338
val iterations = if (intArgs.length > 1) intArgs(1).toInt else 20
3439
val forks = if (intArgs.length > 2) intArgs(2).toInt else 1
35-
40+
val measurementTime = if (intArgs.length > 3) intArgs(3).toInt else 1
3641

3742
import File.{ separator => sep }
3843

@@ -48,7 +53,13 @@ object Bench {
4853
.mode(Mode.AverageTime)
4954
.timeUnit(TimeUnit.MILLISECONDS)
5055
.warmupIterations(warmup)
56+
.warmupTime(TimeValue.seconds(measurementTime))
5157
.measurementIterations(iterations)
58+
.measurementTime(TimeValue.seconds(measurementTime))
59+
// To output results to bench/results.json, uncomment the 2
60+
// following lines and the ResultFormatType import.
61+
//.result("results.json")
62+
//.resultFormat(ResultFormatType.JSON)
5263
.forks(forks)
5364
.build
5465

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package dotty.tools.benchmarks
2+
3+
import java.nio.file.{Files, Paths, Path}
4+
import java.util.Random
5+
6+
/** Generates benchmarks in `genDirName`.
7+
*
8+
* Called automatically by the benchmarks runner ([[Bench.main]]).
9+
*/
10+
def generateBenchmarks(genDirName: String) =
11+
val thisFile = Paths.get("src/main/scala/generateBenchmarks.scala")
12+
val genDir = Paths.get(genDirName)
13+
14+
def generateBenchmark(subDirName: String, fileName: String, make: () => String) =
15+
val outputDir = genDir.resolve(Paths.get(subDirName))
16+
Files.createDirectories(outputDir)
17+
val file = outputDir.resolve(Paths.get(fileName))
18+
if !Files.exists(file) ||
19+
Files.getLastModifiedTime(file).toMillis() <
20+
Files.getLastModifiedTime(thisFile).toMillis() then
21+
println(f"Generate benchmark $file")
22+
Files.write(file, make().getBytes())
23+
24+
// Big compile-time sums of constant integer types: (1.type + 2.type + …).
25+
// This should ideally have a linear complexity.
26+
generateBenchmark("compiletime-ops", "sum-constants.scala", () =>
27+
val innerSum = (1 to 50) // Limited to 50 to avoid stackoverflows in the compiler.
28+
.map(i => f"$i")
29+
.mkString(" + ")
30+
val outerSum = (1 to 50)
31+
.map(_ => f"($innerSum)")
32+
.mkString(" + ")
33+
val vals = (1 to 50)
34+
.map(i => f"val v$i: $outerSum = ???")
35+
.mkString("\n\n ")
36+
37+
f"""
38+
import scala.compiletime.ops.int.*
39+
40+
object Test:
41+
val one: 1 = ???
42+
val n: Int = ???
43+
val m: Int = ???
44+
45+
$vals
46+
"""
47+
)
48+
49+
// Big compile-time sums of term reference types: (one.type + m.type + n.type
50+
// + one.type + m.type + n.type + …). This big type is normalized to (8000 +
51+
// 8000 * m.type + 8000 * n.type).
52+
generateBenchmark("compiletime-ops", "sum-termrefs.scala", () =>
53+
val innerSum = (1 to 40)
54+
.map(_ => "one.type + m.type + n.type")
55+
.mkString(" + ")
56+
val outerSum = (1 to 20)
57+
.map(_ => f"($innerSum)")
58+
.mkString(" + ")
59+
val vals = (1 to 4)
60+
.map(i => f"val v$i: $outerSum = ???")
61+
.mkString("\n\n ")
62+
63+
f"""
64+
import scala.compiletime.ops.int.*
65+
66+
object Test:
67+
val one: 1 = ???
68+
val n: Int = ???
69+
val m: Int = ???
70+
71+
$vals
72+
"""
73+
)
74+
75+
// Big compile-time sums of term references: (n + m + …). The result type is
76+
// inferred. The goal of this benchmark is to measure the performance cost of
77+
// inferring precise types for arithmetic operations.
78+
generateBenchmark("compiletime-ops", "sum-termrefs-terms.scala", () =>
79+
val innerSum = (1 to 40)
80+
.map(_ => "one + m + n")
81+
.mkString(" + ")
82+
val outerSum = (1 to 20)
83+
.map(_ => f"($innerSum)")
84+
.mkString(" + ")
85+
val vals = (1 to 4)
86+
.map(i => f"val v$i = $outerSum")
87+
.mkString("\n\n ")
88+
89+
f"""
90+
import scala.compiletime.ops.int.*
91+
92+
object Test:
93+
val one: 1 = ???
94+
val n: Int = ???
95+
val m: Int = ???
96+
97+
$vals
98+
"""
99+
)
100+
101+
// Big compile-time product of sums of term references: (one + n + m) * (one +
102+
// n + m) * …. The goal of this benchmark is to measure the performance impact
103+
// of distributing addition over multiplication during compile-time operations
104+
// normalization.
105+
generateBenchmark("compiletime-ops", "distribute.scala", () =>
106+
val product = (1 to 18)
107+
.map(_ => "(one.type + m.type + n.type)")
108+
.mkString(" * ")
109+
val vals = (1 to 50)
110+
.map(i => f"val v$i: $product = ???")
111+
.mkString("\n\n ")
112+
113+
f"""
114+
import scala.compiletime.ops.int.*
115+
116+
object Test:
117+
val one: 1 = ???
118+
val n: Int = ???
119+
val m: Int = ???
120+
121+
$vals
122+
"""
123+
)
124+
125+
def applicationCount = 14
126+
def applicationDepth = 10
127+
def applicationVals = 2
128+
129+
// Compile-time sums of big applications: Op[Op[…], Op[…]] + Op[Op[…], Op[…]]
130+
// + …. Applications are deep balanced binary trees only differing in their
131+
// very last (top-right) leafs. These applications are compared pairwise in
132+
// order to sort the terms of the sum.
133+
generateBenchmark("compiletime-ops", "sum-applications.scala", () =>
134+
def makeOp(depth: Int, last: Boolean, k: Int): String =
135+
if depth == 0 then f"Op[one.type, ${if last then k.toString else "n.type"}]"
136+
else f"Op[${makeOp(depth - 1, false, k)}, ${makeOp(depth - 1, last, k)}]"
137+
val sum = (applicationCount to 1 by -1)
138+
.map(k => makeOp(applicationDepth, true, k))
139+
.mkString(" + ")
140+
val vals = (1 to applicationVals)
141+
.map(i => f"val v$i: $sum = ???")
142+
.mkString("\n\n ")
143+
144+
f"""
145+
import scala.compiletime.ops.int.*
146+
147+
object Test:
148+
val one: 1 = ???
149+
val n: Int = ???
150+
type SInt = Int & Singleton
151+
type Op[A <: SInt, B <: SInt] <:SInt
152+
153+
$vals
154+
"""
155+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.compiletime.ops.int.*
2+
3+
object Test:
4+
val one: 1 = ???
5+
val n: Int = ???
6+
val m: Int = ???

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4248,15 +4248,6 @@ object Types {
42484248
case _ => None
42494249
}
42504250

4251-
val opsSet = Set(
4252-
defn.CompiletimeOpsAnyModuleClass,
4253-
defn.CompiletimeOpsIntModuleClass,
4254-
defn.CompiletimeOpsLongModuleClass,
4255-
defn.CompiletimeOpsFloatModuleClass,
4256-
defn.CompiletimeOpsBooleanModuleClass,
4257-
defn.CompiletimeOpsStringModuleClass
4258-
)
4259-
42604251
// Returns Some(true) if the type is a constant.
42614252
// Returns Some(false) if the type is not a constant.
42624253
// Returns None if there is not enough information to determine if the type is a constant.
@@ -4272,7 +4263,7 @@ object Types {
42724263
// constant if the term is constant
42734264
case t: TermRef => isConst(t.underlying)
42744265
// an operation type => recursively check all argument compositions
4275-
case applied: AppliedType if opsSet.contains(applied.typeSymbol.owner) =>
4266+
case applied: AppliedType if defn.isCompiletimeAppliedType(applied.typeSymbol) =>
42764267
val argsConst = applied.args.map(isConst)
42774268
if (argsConst.exists(_.isEmpty)) None
42784269
else Some(argsConst.forall(_.get))

0 commit comments

Comments
 (0)