Java8对具有相同名称的数千个默认方法的接口进行了慢速编译

时间:2016-08-23 22:08:34

标签: java interface java-8 compile-time default-method

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

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个重载方法)需要大约10秒。 编译接口VisitorB和VisitorC需要大约1.5分钟。 但是当我们尝试编译接口VisitorD时,Java 8编译器大约需要7分钟!

  • 有谁知道为什么需要这么多时间来编译VisitorD?
  • 是否因为默认方法的继承?
  • 或者是因为钻石星座,VisitorB和VisitorC扩展了VisitorA和VisitorD再次延伸VisitorB和VisitorC?

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

 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) {...}
}

现在访客D的编制时间只需要2分钟左右。 但这仍然很多。

  • 有没有人知道如何将VisitorD的编译时间减少到几秒钟?
  • 如果我们删除了VisitorD的两个扩展关系extends VisitorBPlain, VisitorCPlain,那么此接口的编译时间大约需要15秒 - 即使它有大约5.000个默认方法。但是我们需要VisitorD与VisitorB和VisitorC兼容(通过直接扩展或与中间Plain接口的间接扩展)以用于构建原因。

我也阅读了类似问题的答案: slow JDK8 compilation 但问题似乎是通用类型推断: "在基于通用目标类型的重载解决方案中,Java 8中存在严重的性能回归。"

所以这有点不同,如果有人会有小费或好消息 解释为什么会这样;我会非常感激。

谢谢你, 迈克尔

3 个答案:

答案 0 :(得分:2)

这个答案归功于@Brian Goetz。

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

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

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

以下是我电脑编译时的截图: VisitorN包含重载和覆盖的visit方法。 VisitorG包含优化的visitX方法,这些方法只会被覆盖但不会再过载。 <code>VisitorN</code> contains overloaded and overwritten <code>visit</code> methods <code>VisitorG</code> contains the optimized <code>visitX</code> methods, which are only overwritten but not overloaded anymore

使用&#34; plain&#34;使用不同visitX方法的方法,然后编译Visitor_SVisitorPlain_S仅需要 22秒(比直接重载{{1}的方法快两倍}} 方法)。 default visitXVisitor_S个方法,但它扩展default没有VisitorPlain_S方法。 default扩展了其他&#34; plain&#34;没有VisitorPlain_S方法的访问者。 <code>Visitor_S</code> has <code>default</code> methods, but it extends <code>VisitorPlain_S</code> having no <code>default</code> methods. <code>VisitorPlain_S</code> extends other "plain" visitors without <code>default</code> methods.

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

答案 1 :(得分:1)

在针对此问题的额外会议之后,我们发现了第一个答案的以下限制:

第一个答案非常适合“静态”访问者,因为它们在ANTLR中使用,因为您没有语言界面,因此visit方法确切地知道孩子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还是调用未知方法{{1}由于语法扩展,将在稍后介绍。这就是为什么我们使用“双重调度”,但因此State#accept方法必须具有相同的名称(因为当我们为{{生成访问者时,我们无法知道不存在的方法visit语法。

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

我们会尝试进一步分析问题,如果我们找到了一个新的方法来生成大型动态访问者,我会及时向您提供最新信息。

答案 2 :(得分:0)

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

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

  • (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(导致此帖子的旧版本)如下所示:

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属于扩展语言H的语言I.语言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编译器分析过程中的一个错误。)