Skip to content

Commit 94eb038

Browse files
jiribenesb-studios
andauthored
Unification benchmark using 'map' and 'stream' (#798)
Resolves #796 It's an OK (not great, not terrible) benchmark: 1. generates two nested, deep pair types like `(...((XLLL, XLLR), (XLRL, XLRR)), ...` for `X = { A, B }`, 2. then unifies them, 3. and checks that the substitutions are like `ALLL -> BLLL`. Configurable with a given `N` if some tweaking is needed. ### Perf: #### My machine On my computer with `N = 12`: - JS compilation only: 4.5s - LLVM compilation only: 5.5s - then hyperfined: ``` $ hyperfine --warmup 5 --min-runs 20 './out/unify-js' './out/unify-llvm' Benchmark 1: ./out/unify-js Time (mean ± σ): 703.0 ms ± 7.8 ms [User: 1035.2 ms, System: 77.1 ms] Range (min … max): 690.9 ms … 721.1 ms 20 runs Benchmark 2: ./out/unify-llvm Time (mean ± σ): 37.6 ms ± 0.5 ms [User: 36.1 ms, System: 1.0 ms] Range (min … max): 37.1 ms … 39.7 ms 69 runs Summary ./out/unify-llvm ran 18.68 ± 0.32 times faster than ./out/unify-js ``` On my computer with `N = 16` (16x more work than `N = 12`) - JS compilation only: 4.7s - LLVM compilation only: 5.6s - then hyperfined: ``` Benchmark 1: ./out/unify-js Time (mean ± σ): 19.982 s ± 0.608 s [User: 26.011 s, System: 1.525 s] Range (min … max): 19.300 s … 21.267 s 20 runs Benchmark 2: ./out/unify-llvm Time (mean ± σ): 928.2 ms ± 20.8 ms [User: 906.3 ms, System: 16.1 ms] Range (min … max): 913.3 ms … 1006.2 ms 20 runs Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. Summary ./out/unify-llvm ran 21.53 ± 0.81 times faster than ./out/unify-js ``` #### CI With `N = 12` in CI (measured imprecisely in two CI rounds): - 9.2-12.0 seconds on Chez backends - 8.0-8.9 seconds on JS - 9.9-12.7 seconds on LLVM With `N = 16` in CI (measured imprecisely in two CI rounds): - 21-29 seconds on Chez backends - 55-57 seconds on JS - 79-86 seconds on LLVM --------- Co-authored-by: Jonathan Brachthäuser <[email protected]>
1 parent 2be5198 commit 94eb038

File tree

4 files changed

+203
-4
lines changed

4 files changed

+203
-4
lines changed

effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ object Normalizer { normal =>
6666
case None => false
6767
}
6868

69+
private def isUnused(id: Id)(using ctx: Context): Boolean =
70+
ctx.usage.get(id).forall { u => u == Usage.Never }
71+
6972
def normalize(entrypoints: Set[Id], m: ModuleDecl, maxInlineSize: Int, preserveBoxing: Boolean): ModuleDecl = {
7073
// usage information is used to detect recursive functions (and not inline them)
7174
val usage = Reachable(entrypoints, m)
@@ -160,6 +163,10 @@ object Normalizer { normal =>
160163

161164
def normalize(s: Stmt)(using C: Context): Stmt = s match {
162165

166+
// see #798 for context (led to stack overflow)
167+
case Stmt.Def(id, block, body) if isUnused(id) =>
168+
normalize(body)
169+
163170
case Stmt.Def(id, block, body) =>
164171
val normalized = active(block).dealiased
165172
Stmt.Def(id, normalized, normalize(body)(using C.bind(id, normalized)))

effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ object TransformerCps extends Transformer {
167167
js.Lambda(vps.map(nameDef) ++ bps.map(nameDef) ++ List(nameDef(ks), nameDef(k)), toJS(body).stmts)
168168
}
169169

170+
def argumentToJS(b: cps.Block)(using TransformerContext): js.Expr = b match {
171+
case cps.BlockLit(vps, bps, ks, k, body) => toJS(b)(using nonrecursive(ks))
172+
case other => toJS(b)
173+
}
174+
170175
def toJS(handler: cps.Implementation)(using TransformerContext): js.Expr = handler match {
171176
case cps.Implementation(interface, operations) =>
172177
js.Object(operations.map {
@@ -191,7 +196,7 @@ object TransformerCps extends Transformer {
191196
case Pure.Literal(s: String) => JsString(escape(s))
192197
case literal: Pure.Literal => js.RawExpr(literal.value.toString)
193198
case DirectApp(id, vargs, Nil) => inlineExtern(id, vargs)
194-
case DirectApp(id, vargs, bargs) => js.Call(nameRef(id), vargs.map(toJS) ++ bargs.map(toJS))
199+
case DirectApp(id, vargs, bargs) => js.Call(nameRef(id), vargs.map(toJS) ++ bargs.map(argumentToJS))
195200
case Pure.PureApp(id, vargs) => inlineExtern(id, vargs)
196201
case Pure.Make(data, tag, vargs) => js.New(nameRef(tag), vargs map toJS)
197202
case Pure.Box(b) => toJS(b)
@@ -331,7 +336,7 @@ object TransformerCps extends Transformer {
331336
stmts.append(js.Assign(nameRef(param), toJS(substitutions.substitute(arg)(using subst))))
332337
}
333338
(bparams zip bargs).foreach { (param, arg) =>
334-
stmts.append(js.Assign(nameRef(param), toJS(substitutions.substitute(arg)(using subst))))
339+
stmts.append(js.Assign(nameRef(param), argumentToJS(substitutions.substitute(arg)(using subst))))
335340
}
336341

337342
// Restore metacont if needed
@@ -343,11 +348,11 @@ object TransformerCps extends Transformer {
343348
}
344349

345350
case cps.Stmt.App(callee, vargs, bargs, ks, k) =>
346-
pure(js.Return(js.Call(toJS(callee), vargs.map(toJS) ++ bargs.map(toJS) ++ List(toJS(ks),
351+
pure(js.Return(js.Call(toJS(callee), vargs.map(toJS) ++ bargs.map(argumentToJS) ++ List(toJS(ks),
347352
requiringThunk { toJS(k) }))))
348353

349354
case cps.Stmt.Invoke(callee, method, vargs, bargs, ks, k) =>
350-
val args = vargs.map(toJS) ++ bargs.map(toJS) ++ List(toJS(ks), toJS(k))
355+
val args = vargs.map(toJS) ++ bargs.map(argumentToJS) ++ List(toJS(ks), toJS(k))
351356
pure(js.Return(MethodCall(toJS(callee), memberNameRef(method), args:_*)))
352357

353358
// const r = ks.arena.newRegion(); body
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Unification successful!
2+
a -> List(Int)
3+
Unification successful!
4+
4096
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/// Robinson-style Unification Algorithm
2+
module examples/benchmarks/unify
3+
4+
import examples/benchmarks/runner
5+
import map
6+
import result
7+
import bytearray
8+
import stream
9+
10+
type Type {
11+
Var(name: String)
12+
Con(name: String, args: List[Type])
13+
}
14+
15+
type Substitution = Map[String, Type]
16+
17+
type UnificationError {
18+
OccursCheckFailure(variable: String, tpe: Type)
19+
UnificationFailure(tpe1: Type, tpe2: Type)
20+
UnificationManyFailure(tps1: List[Type], tps2: List[Type])
21+
}
22+
23+
// Check if a type variable occurs in another type
24+
def occurs(variable: String, ty: Type): Bool = ty match {
25+
case Var(name) => name == variable
26+
case Con(_, args) => args.any { arg => variable.occurs(arg) }
27+
}
28+
29+
// Apply a substitution to a type
30+
def apply(subst: Substitution, ty: Type): Type = ty match {
31+
case Var(name) =>
32+
subst.getOrElse(name) { () => ty }
33+
case Con(name, args) =>
34+
Con(name, args.map { arg => subst.apply(arg) })
35+
}
36+
37+
def unify(ty1: Type, ty2: Type, subst: Substitution): Substitution / Exception[UnificationError] = {
38+
val substTy1 = subst.apply(ty1)
39+
val substTy2 = subst.apply(ty2)
40+
41+
(substTy1, substTy2) match {
42+
// If both are the same variable, return current substitution
43+
case (Var(x), Var(y)) and x == y =>
44+
subst
45+
46+
// If first is a variable, try to bind it
47+
case (Var(x), _) =>
48+
if (x.occurs(substTy2)) {
49+
do raise(OccursCheckFailure(x, substTy2), "")
50+
} else {
51+
subst.put(x, substTy2)
52+
}
53+
54+
// If second is a variable, try to bind it
55+
case (_, Var(y)) =>
56+
if (occurs(y, substTy1)) {
57+
do raise(OccursCheckFailure(y, substTy1), "")
58+
} else {
59+
subst.put(y, substTy1)
60+
}
61+
62+
// If both are constructors, unify their arguments
63+
case (Con(name1, args1), Con(name2, args2)) =>
64+
if (name1 != name2) {
65+
do raise(UnificationFailure(substTy1, substTy2), "Different constructors!")
66+
} else if (args1.size != args2.size) {
67+
do raise(UnificationFailure(substTy1, substTy2), "Different number of arguments!")
68+
} else {
69+
unifyMany(args1, args2, subst)
70+
}
71+
}
72+
}
73+
74+
// Unify a list of arguments with a current substitution
75+
def unifyMany(args1: List[Type], args2: List[Type], subst: Substitution): Substitution / Exception[UnificationError] =
76+
(args1, args2) match {
77+
case (Nil(), Nil()) => subst
78+
case (Cons(a1, rest1), Cons(a2, rest2)) =>
79+
val newSubst = unify(a1, a2, subst)
80+
unifyMany(rest1, rest2, newSubst)
81+
case _ => do raise(UnificationManyFailure(args1, args2), "Different numbers of types on each side!")
82+
}
83+
84+
def unify(ty1: Type, ty2: Type): Substitution / Exception[UnificationError] =
85+
unify(ty1, ty2, map::empty(box bytearray::compareStringBytes))
86+
87+
def showType(ty: Type): String = ty match {
88+
case Var(name) => name
89+
case Con(name, Nil()) => name
90+
case Con(name, args) =>
91+
name ++ "(" ++ args.map { t => showType(t) }.join(", ") ++ ")"
92+
}
93+
94+
def show(err: UnificationError): String = err match {
95+
case OccursCheckFailure(variable, ty) =>
96+
"Occurs check failed: " ++ variable ++ " occurs in " ++ showType(ty)
97+
case UnificationFailure(ty1, ty2) =>
98+
"Cannot unify " ++ showType(ty1) ++ " with " ++ showType(ty2)
99+
case UnificationManyFailure(tps1, tps2) =>
100+
"Cannot unify " ++ tps2.map { showType }.join(", ") ++ " with " ++ tps1.map { showType }.join(", ")
101+
}
102+
103+
/// Worker wrapper
104+
def reporting { body : => Substitution / Exception[UnificationError] }: Unit / emit[(String, Type)] = {
105+
val res = result[Substitution, UnificationError] {
106+
body()
107+
}
108+
109+
res match {
110+
case Success(subst) => {
111+
println("Unification successful!")
112+
subst.each
113+
}
114+
case Error(err, msg) =>
115+
println("Unification failed: " ++ show(err))
116+
if (msg.length > 0) {
117+
println(msg)
118+
}
119+
}
120+
}
121+
122+
/// Used for testing to generate two `depth`-deep, nested types of the shape:
123+
/// ```
124+
/// (Nested
125+
/// (Nested
126+
/// ...
127+
/// XLLLLLLLL
128+
/// XLLLLLLLR)
129+
/// ```
130+
/// for `baseVar = X`.
131+
def generateDeepType(depth: Int, baseVar: String): Type = {
132+
def recur(currentDepth: Int, varSuffix: String): Type =
133+
if (currentDepth == 0) {
134+
Var(baseVar ++ varSuffix)
135+
} else {
136+
Con("Nested", [
137+
recur(currentDepth - 1, varSuffix ++ "L"),
138+
recur(currentDepth - 1, varSuffix ++ "R")
139+
])
140+
}
141+
142+
recur(depth, "")
143+
}
144+
145+
def run(N: Int) = {
146+
def printBinding(pair: (String, Type)): Unit =
147+
println(" " ++ pair.first ++ " -> " ++ showType(pair.second))
148+
149+
// sanity check
150+
for {
151+
reporting {
152+
val intType = Con("Int", [])
153+
val listType = Con("List", [intType])
154+
val typeVar = Var("a")
155+
156+
unify(typeVar, listType)
157+
}
158+
} { printBinding }
159+
160+
// the actual test
161+
var found = 0
162+
163+
for {
164+
reporting {
165+
val deepType1 = generateDeepType(N, "A")
166+
val deepType2 = generateDeepType(N, "B")
167+
unify(deepType1, deepType2)
168+
}
169+
} {
170+
case (l, Var(r)) and l.substring(1) == r.substring(1) =>
171+
found = found + 1
172+
case (l, r) =>
173+
println("error! " ++ l ++ " -> " ++ showType(r))
174+
}
175+
176+
val expected = 2.0.pow(N).toInt
177+
if (found != expected) {
178+
panic("found: " ++ found.show ++ ", but expected: " ++ expected.show)
179+
}
180+
found
181+
}
182+
183+
def main() = benchmark(12){run}

0 commit comments

Comments
 (0)