【问题标题】:Java8 slow compiling for interfaces with thousands of default methods with the same nameJava8 对具有数千个同名默认方法的接口进行慢速编译
【发布时间】:2016-12-30 21:19:07
【问题描述】:

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

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

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

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

我还阅读了类似问题的答案: slow JDK8 compilation 但问题似乎出在泛型类型推断上: “在基于泛型目标类型的重载解析方面,Java 8 中存在严重的性能退化。”

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

谢谢你, 迈克尔

【问题讨论】:

  • 抱歉 - 没有答案,但我很好奇 - 这些文件有多大?我有一个包含大约 1000 个文件的项目,总共大约 150,000 行,而使用 Maven,编译需要 15 秒多一点。您必须有一些主要文件。
  • 这个文件很大。它有大约 270kB 大。我上传了,大家自己看吧:drive.google.com/open?id=0B6L6K365bELNbXFhZVp6MG55RU0
  • 这是由所有具有相同名称的方法引起的。因此,要检查覆盖是否正确(不会意外覆盖桥),必须相互检查每种方法,这会变得二次(或更糟)。在一个类中拥有数千个具有相同名称的方法会给重载选择/检查带来很大压力,这就是您看到这种情况的原因。 (请注意,这在手写代码中并不常见,只是生成的代码。)
  • 将方法命名为visitAst1(AST1)。 (无论如何,大多数访问者都会这样做。)
  • AST1 中的accept(Visitor v) 方法委托给v.visitAst1(this)

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


【解决方案1】:

这个答案归功于@Brian Goetz。

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

    第一个答案非常适合“静态”访问者,因为它们在 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 甚至是未知方法State#accept,稍后会介绍由于语法扩展。这就是我们使用“双重调度”的原因,但因此 visit 方法必须具有相同的名称(因为我们无法知道在为 MontiArc 语法生成访问者时不存在的方法 visitState(StateAST node)

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

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

    【讨论】:

      【解决方案3】:

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

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

      • (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编译器分析过程中的bug。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-09-06
        • 1970-01-01
        • 2018-01-16
        • 2017-08-16
        • 2014-05-06
        • 2018-01-07
        • 2015-08-10
        • 2020-04-20
        相关资源
        最近更新 更多