Java8 медленная компиляция для интерфейсов с тысячами методов по умолчанию с тем же именем

Учитывая интерфейсы (которые очень большие и генерируются из языковых определений):

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 VisitorBPlain, VisitorCPlain тогда время компиляции этого интерфейса должно быть около 15 с - даже если у него около 5 000 методов по умолчанию. Но нам нужно, чтобы VisitorD был совместим с VisitorB и VisitorC (либо прямым расширением, либо косвенным с промежуточными интерфейсами Plain) по причинам приведения.

Я также прочитал ответы на аналогичный вопрос: медленная компиляция JDK8, но там, похоже, проблема заключалась в выводе универсального типа: "В Java 8 наблюдается серьезное снижение производительности, когда речь идет о разрешении перегрузки, основанном на универсальной типизации цели".

Так что это немного по-другому, если у кого-то будет совет или хорошее объяснение, почему это так; Я был бы очень благодарен.

Спасибо майкл

3 ответа

Решение

Мы выяснили, как решить эту проблему для нас: у нас была ошибка в генераторе, поскольку перегруженный унаследованный метод имел то же тело метода, что и унаследованный.

Для нас это означало бы, что у нас есть два способа решить эту проблему:

  • (а) не генерировать методы, которые мы унаследовали больше
  • (б) генерировать все методы, но удалить наследование интерфейса

Интересно то, что (а) нужно больше времени на компиляцию, чем (б).

Я провел эксперимент на своем Mac, чтобы представить результаты, которые мы нашли в процессе исправления, которые вы можете скачать по адресу: https://drive.google.com/open?id=0B6L6K365bELNWDRoeTF4RXJsaFk

Я просто опишу основные файлы эксперимента здесь и результаты. Может быть, кто-нибудь найдет это полезным.

Версия 1 (б) и выглядит так:

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 (а) выглядит следующим образом:

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

Кредит на этот ответ идет на @ Брайан Гетц.

Я создал фиктивный тест, где когда-то все visit методы были перезаписаны и перегружены, в то время как visitX методы получили разные названия.

И результат был более удивительным, чем я думал: при перегрузке и перезаписи visit Способы, компилятору понадобилось почти 30 минут! Когда я переименовал visit методы уникально внутри одного класса посетителя, компилятору потребовалось всего 46 секунд.

Вот исходный код для фиктивного теста: https://drive.google.com/open?id=0B6L6K365bELNUkVYMHZnZ0dGREk

А вот скриншоты для времени компиляции на моем компьютере: VisitorN содержит перегруженные и перезаписанные visit методы. VisitorG содержит оптимизированный visitX методы, которые только перезаписываются, но не перегружаются. code> VisitorN </ code> содержит перегруженные и перезаписанные методы <code> посещения </ code VisitorN содержит перегруженные и перезаписанные методы посещения code> VisitorG </ code> содержит оптимизированные методы <code> visitX </ code>, которые только перезаписываются, но больше не перегружаются VisitorG содержит оптимизированные методы visitX , которые только перезаписываются, но больше не перегружаются>

Использование "простого" подхода с разными visitX методы, затем компиляция Visitor_S а также VisitorPlain_S требуется всего около 22 секунд (будучи в два раза быстрее, чем подход с перегрузкой непосредственно default visitX методы). Visitor_S имеет default методы, но это расширяет VisitorPlain_S не имея default методы. VisitorPlain_S расширяет других "простых" посетителей без default методы. code> Visitor_S </ code> имеет методы <code> default </ code>, но расширяет <code> VisitorPlain_S </ code>, не имея методов <code> default </ code>. <code> VisitorPlain_S </ code> расширяет других default Visitor_S имеет методы default , но расширяет VisitorPlain_S , не имея методов default . VisitorPlain_S расширяет других "простых" посетителей без методов default

Но что я до сих пор не понимаю - только для моего теоретического интереса, факт с методами моста: В https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html только методы моста происходят до стирание типов, но в примере у нас не было обобщений, поэтому стирание типов не должно играть никакой роли. - Может быть, у кого-нибудь есть хорошее объяснение, почему это все еще имеет значение.

После дополнительной встречи, посвященной только этой проблеме, мы выяснили следующие ограничения первого ответа:

Первый ответ очень хорошо работает для "статических" посетителей, так как они используются в 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 методы должны иметь одинаковое имя (так как мы не могли знать метод visitState(StateAST node) которого нет, когда мы генерируем посетителя для MontiArc грамматика.

Мы думали о генерации visitX метод и делегировать этот метод от общего visit метод с использованием большого instanceof-если-каскада. Но для этого потребуется добавить дополнительные if заявления к visit(MontiArcAST node) после развертывания нашего jar-файла грамматики MontiArcи это разрушило бы нашу современность.

Мы попытаемся проанализировать проблему дальше, и я буду держать вас в курсе, если мы найдем новую методологию, как генерировать больших динамических посетителей.

Другие вопросы по тегам