7

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

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 接口(包含大约 2000 个重载方法)大约需要 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) {...}
}

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

  • 有人知道如何将 VisitorD 的编译时间减少到几秒钟吗?
  • 如果我们去掉 VisitorD, 的两个 extends 关系extends VisitorBPlain, VisitorCPlain,那么这个接口的编译时间大约需要 15 秒——尽管它有大约 5.000 个默认方法。但是出于强制转换的原因,我们需要 VisitorD 与 VisitorB 和 VisitorC 兼容(通过直接扩展或具有中间纯接口的间接扩展)。

我还阅读了类似问题的答案: JDK8 编译速度慢 ,但问题似乎在于泛型类型推断:“在基于泛型目标类型的重载解决方面,Java 8 中存在严重的性能退化。”

所以这有点不同,如果有人有一个小费或一个很好的解释为什么会这样;我将非常感谢。

谢谢你,迈克尔

4

3 回答 3

2

这个答案的功劳归功于@Brian Goetz。

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

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

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

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

使用带有不同方法的“普通”visitX方法,然后编译Visitor_S并且VisitorPlain_S只需要大约22 秒(比直接重载方法的方法快两倍default visitX)。 Visitor_Sdefault方法,但它扩展VisitorPlain_S没有default方法。VisitorPlain_S没有方法扩展其他“普通”访问者default<code>Visitor_S</code> 有 <code>default</code> 方法,但它扩展了 <code>VisitorPlain_S</code> 没有 <code>default</code> 方法。 <code>VisitorPlain_S</code> 扩展其他

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

于 2016-08-25T22:50:34.360 回答
1

在为这个问题举行了额外的会议之后,我们发现了第一个答案的以下限制:

第一个答案非常适合“静态”访问者,因为它们在 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 = ... "->" ...;
}

Visitor forMontiArcAST并不确切知道accept应该调用哪个方法,因为您不知道是否应该调用PortAST#accept甚至是 not known method State#accept,由于语法扩展,稍后将介绍。这就是我们使用“双重调度”的原因,但因此visit方法必须具有相同的名称(因为当我们为语法visitState(StateAST node)生成访问者时,我们不知道哪个方法不存在。MontiArc

我们考虑使用大型-if-cascadevisitX从通用方法生成方法并委托给该方法。但这需要在部署我们的语法 jar-File 后添加额外的语句,这会破坏我们的模块化。visitinstanceofifvisit(MontiArcAST node)MontiArc

我们将尝试进一步分析该问题,如果我们找到了一种新的方法来生成大型动态访问者,我会及时通知您。

于 2016-08-26T11:00:17.893 回答
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 属于语言 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。)

于 2017-03-09T19:47:01.490 回答