Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Very slow compile time involving path-dependent types #15525

Closed
pweisenburger opened this issue Jun 26, 2022 · 5 comments · Fixed by #15556
Closed

Very slow compile time involving path-dependent types #15525

pweisenburger opened this issue Jun 26, 2022 · 5 comments · Fixed by #15556

Comments

@pweisenburger
Copy link
Contributor

Compiler version

3.1.2

Minimized code

Given the minimized code:

class /[D, T]
class Delegating[D]

type Aux[E] = Container { type Elements = E }

class Container:
  type Elements = Delegating[Delegates]
  type Delegates

class Resolution[E](value: Aux[E]):
  type Type = Aux[E]

The following code compiles almost instantly (which is expected):

def element0: Container { type Delegates = Unit } = ???

def element16(
    transmittable0: Resolution[?], transmittable1: Resolution[?],
    transmittable2: Resolution[?], transmittable3: Resolution[?],
    transmittable4: Resolution[?], transmittable5: Resolution[?],
    transmittable6: Resolution[?], transmittable7: Resolution[?],
    transmittable8: Resolution[?], transmittable9: Resolution[?],
    transmittable10: Resolution[?], transmittable11: Resolution[?],
    transmittable12: Resolution[?], transmittable13: Resolution[?],
    transmittable14: Resolution[?], transmittable15: Resolution[?])
: Container {
    type Delegates =
      transmittable0.Type / transmittable1.Type /
      transmittable2.Type / transmittable3.Type /
      transmittable4.Type / transmittable5.Type /
      transmittable6.Type / transmittable7.Type /
      transmittable8.Type / transmittable9.Type /
      transmittable10.Type / transmittable11.Type /
      transmittable12.Type / transmittable13.Type /
      transmittable14.Type / transmittable15.Type
  } = ???

def test16 =
  Resolution(
    element16(
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0)))

But the following example, which is just a slightly bigger version of the previous example (22 instead of 16 parameters), takes 10+ seconds to compile on my machine:

def element22(
    transmittable0: Resolution[?], transmittable1: Resolution[?],
    transmittable2: Resolution[?], transmittable3: Resolution[?],
    transmittable4: Resolution[?], transmittable5: Resolution[?],
    transmittable6: Resolution[?], transmittable7: Resolution[?],
    transmittable8: Resolution[?], transmittable9: Resolution[?],
    transmittable10: Resolution[?], transmittable11: Resolution[?],
    transmittable12: Resolution[?], transmittable13: Resolution[?],
    transmittable14: Resolution[?], transmittable15: Resolution[?],
    transmittable16: Resolution[?], transmittable17: Resolution[?],
    transmittable18: Resolution[?], transmittable19: Resolution[?],
    transmittable20: Resolution[?], transmittable21: Resolution[?])
: Container {
    type Delegates =
      transmittable0.Type / transmittable1.Type /
      transmittable2.Type / transmittable3.Type /
      transmittable4.Type / transmittable5.Type /
      transmittable6.Type / transmittable7.Type /
      transmittable8.Type / transmittable9.Type /
      transmittable10.Type / transmittable11.Type /
      transmittable12.Type / transmittable13.Type /
      transmittable14.Type / transmittable15.Type /
      transmittable16.Type / transmittable17.Type /
      transmittable18.Type / transmittable19.Type /
      transmittable20.Type / transmittable21.Type
  } = ???

def test22 =
  Resolution(
    element22(
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0),
      Resolution(element0), Resolution(element0)))

My “real” (not minimized code) has a few more parameters and takes ages to compile (did not finish yet after a several minutes), which makes me think that code in this shape maybe triggers some kind of exponential explosion in the type checker.

Expectation

Scala 2.13 compiles the code instantly. The expectation would be the same for Scala 3.

Note

FYI, I found #14224, which may or may not have the same root cause.

@pweisenburger pweisenburger added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jun 26, 2022
@Kordyjan Kordyjan added area:typer itype:performance and removed itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jun 27, 2022
@Kordyjan
Copy link
Contributor

I tried to reproduce that and it is hanging for me for a few minutes on the current main. This is something nasty.

@Kordyjan
Copy link
Contributor

Oh, I see now! I lost the definition of element0 in my snippet. Without that value, it compiles much much longer before finally failing with a type error.

The correct case is also taking longer to compile but it is not that dramatic.

@pweisenburger
Copy link
Contributor Author

I looked into this a bit more. It seems that isSubType is called with the following arguments:

tp1 = Delegating[?1.Type / ?2.Type / ... / ?15.Type / (?16 : Resolution[Delegating[Unit]])#Type]
tp2 = Delegating[?17.Type / ?18.Type / ... / ?31.Type / ?32.Type]

The ? are the skolems for the method arguments. For some reason that I do not fully understand, type-checking is really slow with all these skolems, in particular the isSubType call at https://github.com/lampepfl/dotty/blob/3.1.2/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L723. The skolems here are only used to select the Type type member. If, at this point, we resolve these type aliases, we end up with an isSubType check:

tp1 = Delegating[(param)1#Type / (param)2#Type / ... / (param)15#Type / (param)16#Type]
tp2 = Delegating[Aux[Delegating[Unit]] / Aux[Delegating[Unit]] / ... / Aux[Delegating[Unit]] / Aux[Delegating[Unit]]]

Type-checking this is as fast as you would expect.

Does it make sense to get rid of <skolem>.SomeAlias early on?

@odersky
Copy link
Contributor

odersky commented Jun 29, 2022

I checked that indeed every increase in element number by one gives twice as many subtype calls. (How?: I turned the inline value Stats.enabled on, compiled with -Ydetailed-stats and looked at the number of "total subtype" entries for various sizes of element.).

The reason, I believe is this: When testing equality A =:= B we test A <:< B and B <:< A. The program here has a deeply nested applied type of an invariant type constructor /. Comparing applied invariant types means testing corresponding type arguments for equality. So at each level we have a branch out factor of 2, for an overall exponential complexity.

odersky added a commit to dotty-staging/dotty that referenced this issue Jun 30, 2022
…ucture

Two deeply applied types with the same structure uses recursive isSameType tests. Each of these
translated to two isSubType tests which can lead to exponential blowup relative to the type's
nesting depth. This problem does not occur if the two types are `eq`. But two types
might still be structurally equal modulo dealiasing.

We now cache isSameType successes after a certain nesting level to avoid recomputation.

Fixes scala#15525.

Reclassifies scala#15365 as a pos test
@odersky
Copy link
Contributor

odersky commented Jun 30, 2022

Does it make sense to get rid of .SomeAlias early on?

This helps in the particular test case because then the two types that are compared hash-cons after dealiasing to the same type. But the fix in #15556 is more general.

@Kordyjan Kordyjan added this to the 3.2.1 milestone Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants