Skip to content

Commit 1aba790

Browse files
authored
Optimize lub algorithm (#20034)
Replace mergeIfSuper by a different algorithm that is more efficient. We drop or-summands in both arguments of a lub that are subsumed by the other. This avoids expensive recursive calls to lub or expensive comparisons with union types on the right. I tested the previous performance regression #19907 with the new algorithm, and without the changes in #19995 that avoid a slow lub. Where previously it took minutes it now compiles fast. Specifically, we get for i19907_slow_1000_3.scala: 2.9s with the optimizations in #19995, 3.3s with just this PR. And for i19907_slow_1000_4.scala: 3.9s with the optimizations in #19995, 4.5s with just this PR. So the optimizations in #19995 are much less critical now since lubs are much faster. Still, it's probably worthwhile to leave them in in case there is a humongous program that stresses lubs even more.
2 parents fecfbda + b9430cb commit 1aba790

File tree

6 files changed

+87
-83
lines changed

6 files changed

+87
-83
lines changed

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

+68-65
Original file line numberDiff line numberDiff line change
@@ -2352,8 +2352,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
23522352
}
23532353

23542354
/** The greatest lower bound of two types */
2355-
def glb(tp1: Type, tp2: Type): Type = /*>|>*/ trace(s"glb(${tp1.show}, ${tp2.show})", subtyping, show = true) /*<|<*/ {
2356-
if (tp1 eq tp2) tp1
2355+
def glb(tp1: Type, tp2: Type): Type = // trace(s"glb(${tp1.show}, ${tp2.show})", subtyping, show = true):
2356+
if tp1 eq tp2 then tp1
23572357
else if !tp1.exists || (tp1 eq WildcardType) then tp2
23582358
else if !tp2.exists || (tp2 eq WildcardType) then tp1
23592359
else if tp1.isAny && !tp2.isLambdaSub || tp1.isAnyKind || isBottom(tp2) then tp2
@@ -2366,12 +2366,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
23662366
val tp2a = dropIfSuper(tp2, tp1)
23672367
if tp2a ne tp2 then glb(tp1, tp2a)
23682368
else tp2 match // normalize to disjunctive normal form if possible.
2369-
case tp2 @ OrType(tp21, tp22) =>
2370-
lub(tp1 & tp21, tp1 & tp22, isSoft = tp2.isSoft)
2369+
case tp2 @ OrType(tp2L, tp2R) =>
2370+
lub(tp1 & tp2L, tp1 & tp2R, isSoft = tp2.isSoft)
23712371
case _ =>
23722372
tp1 match
2373-
case tp1 @ OrType(tp11, tp12) =>
2374-
lub(tp11 & tp2, tp12 & tp2, isSoft = tp1.isSoft)
2373+
case tp1 @ OrType(tp1L, tp1R) =>
2374+
lub(tp1L & tp2, tp1R & tp2, isSoft = tp1.isSoft)
23752375
case tp1: ConstantType =>
23762376
tp2 match
23772377
case tp2: ConstantType =>
@@ -2386,8 +2386,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
23862386
NothingType
23872387
case _ => andType(tp1, tp2)
23882388
case _ => andType(tp1, tp2)
2389+
end mergedGlb
2390+
23892391
mergedGlb(dropExpr(tp1.stripLazyRef), dropExpr(tp2.stripLazyRef))
2390-
}
2392+
end glb
23912393

23922394
def widenInUnions(using Context): Boolean =
23932395
migrateTo3 || ctx.erasedTypes
@@ -2396,14 +2398,23 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
23962398
* @param canConstrain If true, new constraints might be added to simplify the lub.
23972399
* @param isSoft If the lub is a union, this determines whether it's a soft union.
23982400
*/
2399-
def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = /*>|>*/ trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) /*<|<*/ {
2400-
if (tp1 eq tp2) tp1
2401+
def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = // trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true):
2402+
if tp1 eq tp2 then tp1
24012403
else if !tp1.exists || (tp2 eq WildcardType) then tp1
24022404
else if !tp2.exists || (tp1 eq WildcardType) then tp2
24032405
else if tp1.isAny && !tp2.isLambdaSub || tp1.isAnyKind || isBottom(tp2) then tp1
24042406
else if tp2.isAny && !tp1.isLambdaSub || tp2.isAnyKind || isBottom(tp1) then tp2
24052407
else
2406-
def mergedLub(tp1: Type, tp2: Type): Type = {
2408+
def mergedLub(tp1: Type, tp2: Type): Type =
2409+
// First, if tp1 and tp2 are the same singleton type, return one of them.
2410+
if tp1.isSingleton && isSubType(tp1, tp2, whenFrozen = !canConstrain) then
2411+
return tp2
2412+
if tp2.isSingleton && isSubType(tp2, tp1, whenFrozen = !canConstrain) then
2413+
return tp1
2414+
2415+
// Second, handle special cases when tp1 and tp2 are disjunctions of
2416+
// singleton types. This saves time otherwise spent in
2417+
// costly subtype comparisons performed in dropIfSub below.
24072418
tp1.atoms match
24082419
case Atoms.Range(lo1, hi1) if !widenInUnions =>
24092420
tp2.atoms match
@@ -2413,20 +2424,24 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
24132424
if (hi1 & hi2).isEmpty then return orType(tp1, tp2, isSoft = isSoft)
24142425
case none =>
24152426
case none =>
2416-
val t1 = mergeIfSuper(tp1, tp2, canConstrain)
2417-
if (t1.exists) return t1
24182427

2419-
val t2 = mergeIfSuper(tp2, tp1, canConstrain)
2420-
if (t2.exists) return t2
2421-
2422-
def widen(tp: Type) = if (widenInUnions) tp.widen else tp.widenIfUnstable
2428+
// Third, try to simplify after widening as follows:
2429+
// 1. Drop all or-factors in tp2 that are subtypes of an or-factor
2430+
// in tp1, yielding tp2Final.
2431+
// 2. Drop all or-factors in tp1 that are subtypes of an or-factor
2432+
// in tp2Final, yielding tp1Final.
2433+
// 3. Combine the two final types in an OrType
2434+
def widen(tp: Type) =
2435+
if widenInUnions then tp.widen else tp.widenIfUnstable
24232436
val tp1w = widen(tp1)
24242437
val tp2w = widen(tp2)
2425-
if ((tp1 ne tp1w) || (tp2 ne tp2w)) lub(tp1w, tp2w, canConstrain = canConstrain, isSoft = isSoft)
2426-
else orType(tp1w, tp2w, isSoft = isSoft) // no need to check subtypes again
2427-
}
2438+
val tp2Final = dropIfSub(tp2w, tp1w, canConstrain)
2439+
val tp1Final = dropIfSub(tp1w, tp2Final, canConstrain)
2440+
recombine(tp1Final, tp2Final, orType(_, _, isSoft = isSoft))
2441+
end mergedLub
2442+
24282443
mergedLub(dropExpr(tp1.stripLazyRef), dropExpr(tp2.stripLazyRef))
2429-
}
2444+
end lub
24302445

24312446
/** Try to produce joint arguments for a lub `A[T_1, ..., T_n] | A[T_1', ..., T_n']` using
24322447
* the following strategies:
@@ -2488,60 +2503,48 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
24882503
Nil
24892504
}
24902505

2491-
private def recombineAnd(tp: AndType, tp1: Type, tp2: Type) =
2492-
if (!tp1.exists) tp2
2493-
else if (!tp2.exists) tp1
2494-
else tp.derivedAndType(tp1, tp2)
2506+
private def recombine(tp1: Type, tp2: Type, rebuild: (Type, Type) => Type): Type =
2507+
if !tp1.exists then tp2
2508+
else if !tp2.exists then tp1
2509+
else rebuild(tp1, tp2)
2510+
2511+
private def recombine(tp1: Type, tp2: Type, tp: AndOrType): Type =
2512+
recombine(tp1, tp2, tp.derivedAndOrType)
24952513

24962514
/** If some (&-operand of) `tp` is a supertype of `sub` replace it with `NoType`.
24972515
*/
24982516
private def dropIfSuper(tp: Type, sub: Type): Type =
2499-
if (isSubTypeWhenFrozen(sub, tp)) NoType
2500-
else tp match {
2517+
2518+
def isSuperOf(sub: Type): Boolean = sub match
2519+
case AndType(sub1, sub2) => isSuperOf(sub1) || isSuperOf(sub2)
2520+
case sub: TypeVar if sub.isInstantiated => isSuperOf(sub.inst)
2521+
case _ => isSubTypeWhenFrozen(sub, tp)
2522+
2523+
tp match
25012524
case tp @ AndType(tp1, tp2) =>
2502-
recombineAnd(tp, dropIfSuper(tp1, sub), dropIfSuper(tp2, sub))
2525+
recombine(dropIfSuper(tp1, sub), dropIfSuper(tp2, sub), tp)
2526+
case tp: TypeVar if tp.isInstantiated =>
2527+
dropIfSuper(tp.inst, sub)
25032528
case _ =>
2504-
tp
2505-
}
2529+
if isSuperOf(sub) then NoType else tp
2530+
end dropIfSuper
25062531

2507-
/** Merge `t1` into `tp2` if t1 is a subtype of some &-summand of tp2.
2508-
*/
2509-
private def mergeIfSub(tp1: Type, tp2: Type): Type =
2510-
if (isSubTypeWhenFrozen(tp1, tp2)) tp1
2511-
else tp2 match {
2512-
case tp2 @ AndType(tp21, tp22) =>
2513-
val lower1 = mergeIfSub(tp1, tp21)
2514-
if (lower1 eq tp21) tp2
2515-
else if (lower1.exists) lower1 & tp22
2516-
else {
2517-
val lower2 = mergeIfSub(tp1, tp22)
2518-
if (lower2 eq tp22) tp2
2519-
else if (lower2.exists) tp21 & lower2
2520-
else NoType
2521-
}
2522-
case _ =>
2523-
NoType
2524-
}
2532+
/** If some (|-operand of) `tp` is a subtype of `sup` replace it with `NoType`. */
2533+
private def dropIfSub(tp: Type, sup: Type, canConstrain: Boolean): Type =
25252534

2526-
/** Merge `tp1` into `tp2` if tp1 is a supertype of some |-summand of tp2.
2527-
* @param canConstrain If true, new constraints might be added to make the merge possible.
2528-
*/
2529-
private def mergeIfSuper(tp1: Type, tp2: Type, canConstrain: Boolean): Type =
2530-
if (isSubType(tp2, tp1, whenFrozen = !canConstrain)) tp1
2531-
else tp2 match {
2532-
case tp2 @ OrType(tp21, tp22) =>
2533-
val higher1 = mergeIfSuper(tp1, tp21, canConstrain)
2534-
if (higher1 eq tp21) tp2
2535-
else if (higher1.exists) lub(higher1, tp22, isSoft = tp2.isSoft)
2536-
else {
2537-
val higher2 = mergeIfSuper(tp1, tp22, canConstrain)
2538-
if (higher2 eq tp22) tp2
2539-
else if (higher2.exists) lub(tp21, higher2, isSoft = tp2.isSoft)
2540-
else NoType
2541-
}
2535+
def isSubOf(sup: Type): Boolean = sup match
2536+
case OrType(sup1, sup2) => isSubOf(sup1) || isSubOf(sup2)
2537+
case sup: TypeVar if sup.isInstantiated => isSubOf(sup.inst)
2538+
case _ => isSubType(tp, sup, whenFrozen = !canConstrain)
2539+
2540+
tp match
2541+
case tp @ OrType(tp1, tp2) =>
2542+
recombine(dropIfSub(tp1, sup, canConstrain), dropIfSub(tp2, sup, canConstrain), tp)
2543+
case tp: TypeVar if tp.isInstantiated =>
2544+
dropIfSub(tp.inst, sup, canConstrain)
25422545
case _ =>
2543-
NoType
2544-
}
2546+
if isSubOf(sup) then NoType else tp
2547+
end dropIfSub
25452548

25462549
/** There's a window of vulnerability between ElimByName and Erasure where some
25472550
* ExprTypes `=> T` that appear as parameters of function types are not yet converted

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

+2-9
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,8 @@ object TypeOps:
157157
tp.derivedAlias(simplify(tp.alias, theMap))
158158
case AndType(l, r) if !ctx.mode.is(Mode.Type) =>
159159
simplify(l, theMap) & simplify(r, theMap)
160-
case tp @ OrType(l, r)
161-
if !ctx.mode.is(Mode.Type)
162-
&& (tp.isSoft || l.isBottomType || r.isBottomType) =>
163-
// Normalize A | Null and Null | A to A even if the union is hard (i.e.
164-
// explicitly declared), but not if -Yexplicit-nulls is set. The reason is
165-
// that in this case the normal asSeenFrom machinery is not prepared to deal
166-
// with Nulls (which have no base classes). Under -Yexplicit-nulls, we take
167-
// corrective steps, so no widening is wanted.
168-
simplify(l, theMap) | simplify(r, theMap)
160+
case tp @ OrType(l, r) if !ctx.mode.is(Mode.Type) =>
161+
TypeComparer.lub(simplify(l, theMap), simplify(r, theMap), isSoft = tp.isSoft)
169162
case tp @ CapturingType(parent, refs) =>
170163
if !ctx.mode.is(Mode.Type)
171164
&& refs.subCaptures(parent.captureSet, frozen = true).isOK

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

+5-6
Original file line numberDiff line numberDiff line change
@@ -3606,12 +3606,11 @@ object Types extends TypeUtils {
36063606

36073607
override def widenUnionWithoutNull(using Context): Type =
36083608
if myUnionPeriod != ctx.period then
3609-
myUnion =
3610-
if isSoft then
3611-
TypeComparer.lub(tp1.widenUnionWithoutNull, tp2.widenUnionWithoutNull, canConstrain = true, isSoft = isSoft) match
3612-
case union: OrType => union.join
3613-
case res => res
3614-
else derivedOrType(tp1.widenUnionWithoutNull, tp2.widenUnionWithoutNull, soft = isSoft)
3609+
val union = TypeComparer.lub(
3610+
tp1.widenUnionWithoutNull, tp2.widenUnionWithoutNull, canConstrain = isSoft, isSoft = isSoft)
3611+
myUnion = union match
3612+
case union: OrType if isSoft => union.join
3613+
case _ => union
36153614
if !isProvisional then myUnionPeriod = ctx.period
36163615
myUnion
36173616

compiler/src/dotty/tools/dotc/typer/Namer.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -1961,7 +1961,8 @@ class Namer { typer: Typer =>
19611961
else
19621962
// don't strip @uncheckedVariance annot for default getters
19631963
TypeOps.simplify(tp.widenTermRefExpr,
1964-
if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null) match
1964+
if defaultTp.exists then TypeOps.SimplifyKeepUnchecked() else null)
1965+
match
19651966
case ctp: ConstantType if sym.isInlineVal => ctp
19661967
case tp => TypeComparer.widenInferred(tp, pt, widenUnions = true)
19671968

tests/pos/i10693.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def test[A, B](a: A, b: B): A | B = a
2+
val v0 = test("string", 1)
3+
val v1 = test(1, "string")
4+
val v2 = test(v0, v1)
5+
val v3 = test(v1, v0)
6+
val v4 = test(v2, v3)
7+
val v5 = test(v3, v2)
8+
val v6 = test(v4, v5)

tests/semanticdb/metac.expect

+2-2
Original file line numberDiff line numberDiff line change
@@ -2020,7 +2020,7 @@ Symbols:
20202020
example/InstrumentTyper# => class InstrumentTyper extends Object { self: AnyRef & InstrumentTyper => +5 decls }
20212021
example/InstrumentTyper#AnnotatedType# => type AnnotatedType = Int @param
20222022
example/InstrumentTyper#`<init>`(). => primary ctor <init> (): InstrumentTyper
2023-
example/InstrumentTyper#all(). => method all => List[Float | Double | List[Nothing] | Boolean | Unit | Char | String | LinkOption | Int | Long | Class[Option[Int]]]
2023+
example/InstrumentTyper#all(). => method all => List[Char | String | LinkOption | Int | Long | Class[Option[Int]] | Float | Double | Boolean | Unit | List[Nothing]]
20242024
example/InstrumentTyper#clazzOf. => final val method clazzOf Option[Int]
20252025
example/InstrumentTyper#singletonType(). => method singletonType (param x: Predef.type): Nothing
20262026
example/InstrumentTyper#singletonType().(x) => param x: Predef.type
@@ -2082,7 +2082,7 @@ Occurrences:
20822082
[24:37..24:40): Int -> scala/Int#
20832083

20842084
Synthetics:
2085-
[8:12..8:16):List => *.apply[Float | Double | List[Nothing] | Boolean | Unit | Char | String | LinkOption | Int | Long | Class[Option[Int]]]
2085+
[8:12..8:16):List => *.apply[Char | String | LinkOption | Int | Long | Class[Option[Int]] | Float | Double | Boolean | Unit | List[Nothing]]
20862086
[20:4..20:8):List => *.apply[Nothing]
20872087

20882088
expect/InventedNames.scala

0 commit comments

Comments
 (0)