Java8 具有数千个同名默认方法的接口编译缓慢

Java8 slow compiling for interfaces with thousands of default methods with the same name

鉴于接口(非常大并且由语言定义生成):

interface VisitorA {
   default void visit(ASTA1 node) {...}
   ...
   default void visit(ASTA2000 node) {...}
}

interface VisitorB extends VisitorA {
   default void visit(ASTB1 node) {...}
   ...
   default void visit(ASTB1000 node) {...}

   // due to language embedding all visit methods of VisitorA
   // must be overwritten
   @Override
   default void visit(ASTA1 node) {...}
   ...
   @Override
   default void visit(ASTA2000 node) {...}
}

interface VisitorC extends VisitorA {
   default void visit(ASTC1 node) {...}
   ...
   default void visit(ASTC1000 node) {...}

   // due to language embedding all visit methods of VisitorA
   // must be overwritten
   @Override
   default void visit(ASTA1 node) {...}
   ...
   @Override
   default void visit(ASTA2000 node) {...}
}

interface VisitorD extends VisitorB, VisitorC {
   default void visit(ASTD1 node) {...}
   ...
   default void visit(ASTD1000 node) {...}

   // due to language embedding all visit methods of VisitorA,
   // VisitorB, and VisitorC must be overwritten
   @Override
   default void visit(ASTA1 node) {...}
   ...
   @Override
   default void visit(ASTA2000 node) {...}

   @Override
   default void visit(ASTB1 node) {...}
   ...
   @Override
   default void visit(ASTB1000 node) {...}

   @Override
   default void visit(ASTC1 node) {...}
   ...
   @Override
   default void visit(ASTC1000 node) {...}
}

现在编译接口VisitorA(包含大约2.000个重载方法)大约需要10s。 编译接口 VisitorB 和 VisitorC 各需要大约 1.5 分钟。 但是当我们尝试编译接口VisitorD时,Java8编译需要7分钟左右!

我们已经尝试过,以下解决方案有所帮助:

 interface VisitorAPlain {
   void visit(ASTA1 node);
   ...
   void visit(ASTA2000 node);
}

interface VisitorA extends VisitorAPlain {
   ... // has same default methods as VisitorA above
}

interface VisitorBPlain extends VisitorAPlain {
   void visit(ASTB1 node);
   ...
   void visit(ASTB1000 node);
}

interface VisitorB extends VisitorBPlain {
   ... // has same default methods as VisitorB above
}

interface VisitorCPlain extends VisitorAPlain {
   void visit(ASTC1 node);
   ...
   void visit(ASTC1000 node);
}

interface VisitorC extends VisitorCPlain {
   ... // has same default methods as VisitorC above
}

interface VisitorD extends VisitorBPlain, VisitorCPlain {
   default void visit(ASTD1 node) {...}
   ...
   default void visit(ASTD1000 node) {...}

   // due to language embedding all visit methods of VisitorAPlain,
   // VisitorBPlain, and VisitorCPlain must be overwritten
   @Override
   default void visit(ASTA1 node) {...}
   ...
   default void visit(ASTA2000 node) {...}

   @Override
   default void visit(ASTB1 node) {...}
   ...
   default void visit(ASTB1000 node) {...}

   @Override
   default void visit(ASTC1 node) {...}
   ...
   default void visit(ASTC1000 node) {...}
}

而现在visitorD的编译时间只需要2分钟左右 但这还是很多。

我也看过类似问题的答案: 但问题似乎出在泛型类型推断上: "There's a severe performance regression in Java 8 when it comes to overload resolution based on generic target typing."

所以这有点不同,如果有人有小费或好的 解释为什么会这样;我将不胜感激。

谢谢, 迈克尔

此答案归功于@Brian Goetz。

我创建了一个虚拟测试,一旦所有 visit 方法都被覆盖和重载,而在另一时间 visitX 方法有不同的名称。

结果比我想象的更惊人: 在重载和覆盖 visit 方法时,编译器需要将近 30 分钟! 当我在一个访问者 class 中唯一重命名 visit 方法时,编译器只需要 46 秒 .

这是虚拟测试的源代码: https://drive.google.com/open?id=0B6L6K365bELNUkVYMHZnZ0dGREk

下面是我电脑上编译时的截图: VisitorN 包含重载和覆盖的 visit 方法。 VisitorG 包含优化的 visitX 方法,这些方法只是被覆盖但不再重载。

使用"plain"方法和不同的visitX方法,然后编译Visitor_SVisitorPlain_S只需要大约22秒 (比直接重载 default visitX 方法的方法快两倍)。 Visitor_Sdefault 方法,但它扩展了 VisitorPlain_S 没有 default 方法。 VisitorPlain_S 扩展了其他 "plain" 没有 default 方法的访问者。

但我仍然不明白——只是出于我的理论兴趣,是桥接方法的事实: 在 https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html 桥接方法中只出现类型擦除,但在示例中我们没有泛型,因此类型擦除根本不应该起作用。 - 也许任何人都能很好地解释为什么它仍然很重要。

经过针对这个问题的额外会议,我们发现了第一个答案的以下局限性:

第一个答案对 "static" 访问者非常有用,因为它们在 ANTLR 中使用,因为那里没有语言界面,所以 visit 方法确切地知道 children ASTTypes。在 MontiCore 中我们可以定义一个接口语法元素,现在将在这里解释:

grammar MontiArc {
  MontiArc = "component" Name "{" ArcElement* "}";
  interface ArcElement;
  Port implements ArcElement = "port" ... ;
}

grammar MontiArcAutomaton extends MontiArc {
  Automaton implements ArcElement = State | Transition;
  State = "state" ... ;
  Transition = ... "->" ...;
}

MontiArcAST 的访问者不知道应该调用哪个 accept 方法,因为您不知道是否应该调用 PortAST#accept 甚至不知道的方法 State#accept,由于语法扩展,后面会介绍。这就是我们使用 "double dispatching" 的原因,但因此 visit 方法必须具有相同的名称(因为我们无法知道方法 visitState(StateAST node),当我们为 MontiArc语法。

我们考虑生成 visitX 方法并使用大型 instanceof-if-cascade 从一般 visit 方法委托给此方法。但这需要在部署语法 MontiArc 的 jar-File 之后向 visit(MontiArcAST node) 添加额外的 if 语句,这会破坏我们的模块性。

我们将尝试进一步分析问题,如果我们找到一种新的方法来生成大量动态访问者,我会留给您up-to-date。

我们想出了解决问题的方法: 我们在生成器中有一个错误,因为重载的继承方法有 与继承自的方法体相同。

这对我们来说意味着我们有两种解决方法:

  • (a) 不再生成我们继承的方法
  • (b) 生成所有方法,但删除接口继承

有趣的是 (a)(b).

需要更多的编译时间

我在 Mac 上做了一个实验来表示我们在修复过程中发现的结果,您可以在以下网址下载: https://drive.google.com/open?id=0B6L6K365bELNWDRoeTF4RXJsaFk

我这里只是描述实验的基本文件,以及结果。也许有人觉得它有用。

版本 1 是 (b),看起来像:

DelegatorVisitorA.java

interface DelegatorVisitorA extends VisitorA {
  VisitorA getVisitorA();  

  default void visit(AST_A1 node) {
    getVisitorA().visit(node);
  }
  ...
  default void visit(AST_A49 node) {
    getVisitorA().visit(node);
  }
}

DelegatorVisitorB.java

interface DelegatorVisitorB extends VisitorB {
  VisitorA getVisitorA();  
  default void visit(AST_A1 node) {
    getVisitorA().visit(node);
  }
  ...
  default void visit(AST_A49 node) {
    getVisitorA().visit(node);
  }
  VisitorB getVisitorB();  

  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

DelegatorVisitorC.java

interface DelegatorVisitorC extends VisitorC {
  VisitorA getVisitorA();
  default void visit(AST_A1 node) {
    getVisitorA().visit(node);
  }
  ...
  default void visit(AST_A49 node) {
    getVisitorA().visit(node);
  }
  VisitorB getVisitorB();  
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
  VisitorC getVisitorC();  
  default void visit(AST_C1 node) {
    getVisitorC().visit(node);
  }
  ...
  default void visit(AST_C49 node) {
    getVisitorC().visit(node);
  }
}

版本 2 是 (a),看起来像:

DelegatorVisitorA.java 与版本 1 相同

DelegatorVisitorB.java

interface DelegatorVisitorB extends VisitorB , DelegatorVisitorA{
  VisitorB getVisitorB();
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

DelegatorVisitorC.java

interface DelegatorVisitorC extends VisitorC , DelegatorVisitorB{
  VisitorB getVisitorB();
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

版本 3(我们有一个中间步骤,但它也是错误的)看起来像:

DelegatorVisitorA.java 与版本 1 相同

DelegatorVisitorB.java

interface DelegatorVisitorB extends VisitorB , DelegatorVisitorA{
  VisitorB getVisitorB();
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

DelegatorVisitorC.java

interface DelegatorVisitorC extends VisitorC , DelegatorVisitorA, DelegatorVisitorB{
  VisitorB getVisitorB();
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

版本 4(导致此 post 的旧版本)看起来像:

DelegatorVisitorA.java 与版本 1 相同

DelegatorVisitorB.java

interface DelegatorVisitorB extends VisitorB , DelegatorVisitorA{
  VisitorA getVisitorA();  
  default void visit(AST_A1 node) {
    getVisitorA().visit(node);
  }
  ...
  default void visit(AST_A49 node) {
    getVisitorA().visit(node);
  }
  VisitorB getVisitorB();  

  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
}

DelegatorVisitorC.java

interface DelegatorVisitorC extends VisitorB , DelegatorVisitorA, DelegatorVisitorB{
  VisitorA getVisitorA();
  default void visit(AST_A1 node) {
    getVisitorA().visit(node);
  }
  ...
  default void visit(AST_A49 node) {
    getVisitorA().visit(node);
  }
  VisitorB getVisitorB();  
  default void visit(AST_B1 node) {
    getVisitorB().visit(node);
  }
  ...
  default void visit(AST_B49 node) {
    getVisitorB().visit(node);
  }
  VisitorC getVisitorC();  
  default void visit(AST_C1 node) {
    getVisitorC().visit(node);
  }
  ...
  default void visit(AST_C49 node) {
    getVisitorC().visit(node);
  }
}

这里我只展示了不同版本的DelegatorVisitorA.java、DelegatorVisitorB.java和DelegatorVisitorC.java。 其他委托人访问者 DelegatorVisitorD.java 到 DelegatorVisitorI.java 遵循相同的模式。 (DelegatorVisitorI属于语言I扩展了语言H,语言H有DelegatorVisitorH,语言H扩展了语言G,以此类推。)

编译DelegatorVisitorI.java的结果在上述四个不同的版本中生成需要很长时间:

结果为:

Version 1:
103-240:srcV1 michael$ time javac DelegatorVisitorI.java

real    0m1.859s
user    0m5.023s
sys 0m0.175s



Version 2:
103-240:srcV2 michael$ time javac DelegatorVisitorI.java

real    0m3.364s
user    0m7.713s
sys 0m0.342s



Version 3:
103-240:srcV3 michael$ time javac DelegatorVisitorI.java

real    2m58.009s
user    2m56.787s
sys 0m1.718s



Version 4:
103-240:srcV4 michael$ time javac DelegatorVisitorI.java

real    14m14.923s
user    14m3.738s
sys 0m5.141s

所有四个不同版本的 Java 文件具有相同的行为, 但由于代码重复,编译过程需要更长的时间。

另外有趣的是,如果你复制方法而不使用任何继承,那么编译是最快的,即使文件在很长的继承链之后变得更大。

(版本2和版本3的时间差大我个人无法理解,可能是javac编译器解析过程中的bug)