Статьи

Автоматическое создание оптимизированных специализаций Java-классов

Если вы посетили JavaOne в этом году, вы, возможно, посетили бы мою презентацию «Как создать настраиваемый код Java 8 из вашей базы данных». В этом выступлении я продемонстрировал, как набор инструментов Speedment Open Source используется для генерации всех видов кода Java с использованием базы данных в качестве модели предметной области. Однако у нас не было времени вдаваться в то, что Speedment не только облегчает генерацию кода, но и сам по себе состоит из сгенерированного кода. В этой статье я покажу вам, как мы настроили Speedment для генерации специализированных версий многих классов, чтобы уменьшить объем памяти критически важных для системы частей системы.

Фон

Как вы, возможно, знаете, в Java есть несколько встроенных типов значений. Это байты, шорты, целые числа, длинные, плавающие, двойные, логические значения и символы. Примитивные типы значений отличаются от обычных объектов в первую очередь тем, что их можно размещать непосредственно в стеке памяти, что снижает нагрузку на сборщик мусора. Проблема отсутствия наследования от Object заключается в том, что они не могут быть помещены в коллекции или переданы в качестве параметров методам, которые принимают параметры объекта без переноса . Типичные классы-обертки, следовательно, «Integer», «Double», «Boolean» и т. Д.

Упаковка типа значения не всегда плохая вещь. JIT (Just-In-Time) компилятор очень хорош в оптимизации типов обёрток, если их можно безопасно заменить примитивными значениями, но это не всегда возможно. Если это происходит в критической части вашего кода, например во внутреннем цикле, это может сильно повлиять на производительность всего приложения.

Вот что случилось с нами при работе над Speedment. У нас были специальные предикаты и функции, которые содержали метаданные об их назначении. Эти метаданные нужно было очень быстро анализировать во внутреннем цикле, но мы были замедлены тем фактом, что большая часть этих метаданных была заключена в универсальные типы, чтобы их можно было создавать динамически.

Распространенным решением этой проблемы является создание ряда «специализаций» классов, которые содержат типы-оболочки. Специализации идентичны исходному классу, за исключением того, что они используют один из типов примитивных значений вместо универсального (только для объекта) типа. Хорошим примером специализаций являются различные интерфейсы Stream, которые существуют в Java 8. В дополнение к «Stream» у нас также есть «IntStream», «DoubleStream» и «LongStream». Эти специализации более эффективны для их конкретного типа значения, так как им не нужно полагаться на обтекание типов в объектах.

Проблема с классами специализации состоит в том, что они добавляют много стандартного в систему. Скажем, что части, которые нужно оптимизировать, состоят из 20 компонентов. Если вы хотите поддерживать все 8 примитивных вариантов, которые есть в Java, у вас вдруг будет 160 компонентов. Это много кода для поддержки. Лучшим решением было бы создать все дополнительные классы.

Генерация кода на основе шаблонов

Самая распространенная форма генерации кода на высших языках — это шаблон. Это означает, что вы пишете файл шаблона, а затем делаете замену ключевых слов, чтобы изменить текст в зависимости от того, что вы генерируете. Хорошими примерами этого являются Maven Archetypes или Thymeleaf . Хороший шаблонизатор будет поддерживать более продвинутый синтаксис, такой как повторяющиеся разделы, выражения условий и т. Д. Если вы хотите генерировать классы специализации с помощью шаблонизатора, вы должны заменить все вхождения «int», «Integer», «IntStream» определенным ключевое слово типа «$ {primitive}», «$ {wrapper}», «$ {stream}», а затем укажите словарь слов для связи с каждым новым типом значения.

Преимущества генерации кода на основе шаблонов в том, что он прост в настройке и обслуживании. Я думаю, что большинство программистов, которые читают это, могли бы понять, как написать шаблонизатор довольно легко. Недостатком является то, что шаблоны трудно использовать повторно. Скажем, у вас есть базовый шаблон для специализированных, но вы хотите, чтобы плавающие типы также имели дополнительный метод. Вы можете решить это с помощью условного оператора, но если вы хотите, чтобы этот дополнительный метод существовал и в других местах, вам нужно будет продублировать код. Типичным примером кода, который часто нужно дублировать, является hashCode () — method или toString (). Вот где генерация кода на основе моделей сильнее.

Генерация кода на основе модели

При генерации кода на основе модели вы строите абстрактное синтаксическое дерево поверх сгенерированного кода, а затем визуализируете это дерево с помощью подходящего средства визуализации. Дерево синтаксиса может быть изменено в зависимости от контекста, в котором оно используется, например, путем добавления или удаления методов для реализации определенного интерфейса. Основным преимуществом этого является более высокая гибкость. Вы можете динамически взять существующую модель и управлять, какие методы и поля включить. Недостатком является то, что генерация кода на основе модели обычно занимает немного больше времени для настройки.

Пример из практики: Полевой генератор Speedment

В Speedment мы разработали генератор кода под названием CodeGen, который использует подход на основе моделей для автоматической генерации специализаций полей для всех типов примитивных значений. Всего на каждой сборке генерируется около 300 классов.

Speedment CodeGen использует абстрактное синтаксическое дерево, построенное вокруг базовых концепций объектно-ориентированного проектирования. У вас есть классы, интерфейсы, поля, методы, конструкторы и т. Д., Которые вы используете для построения модели предметной области. Ниже уровня метода вам все еще нужно написать шаблонный код. Чтобы определить новый основной класс, вы должны написать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.speedment.common.codegen.model.Class; // Not java.lang.Class
 
...
 
Class createMainClass() {
  return Class.of("Main")
    .public_().final_()
    .set(Javadoc.of("The main entry point of the application")
      .add(AUTHOR.setValue("Emil Forslund"))
      .add(SINCE.setValue("1.0.0"))
    )
    .add(Method.of("main", void.class)
      .public_().static_()
      .add(Field.of("args", String[].class))
      .add(
        "if (args.length == 0) " + block(
          "System.out.println(\"Hello, World!\");"
        ) + " else " + block(
          "System.out.format(\"Hi, %s!%n\", args[0]);"
        )
      )
    );
}

Это сгенерирует следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * The main entry point of the application.
 *
 * @author Emil Forslund
 * @since  1.0.0
 */
public final class Main {
  public static void main(String[] args) {
    if (args.length == 0) {
      System.out.println("Hello, World!");
    } else {
      System.out.format("Hi, %s!%n", args[0]);
    }
  }
}

Вся модель не должна быть сгенерирована сразу. Если мы, например, хотим автоматически генерировать метод toString (), мы можем определить его как отдельный метод.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public void generateToString(File file) {
  file.add(Import.of(StringBuilder.class));
  file.getClasses().stream()
    .filter(HasFields.class::isInstance)
    .filter(HasMethods.class::isInstance)
    .map(c -> (HasFields & HasMethods) c)
    .forEach(clazz ->
      clazz.add(Method.of("toString", void.class)
        .add(OVERRIDE)
        .public_()
        .add("return new StringBuilder()")
        .add(clazz.getFields().stream()
          .map(f -> ".append(\"" + f.getName() + "\")")
          .map(Formatting::indent)
          .toArray(String[]::new)
        )
        .add(indent(".toString();"))
      )
    );
}

Здесь вы можете увидеть, как используется шаблон Trait для отделения базовой реализации от логики. Код будет работать как для Enum, так и для Class, так как оба реализуют черты «HasFields» и «HasMethods».

Резюме

В этой статье я объяснил, что такое классы специализации и почему они иногда необходимы для повышения производительности в критических разделах приложения. Я также показал вам, как Speedment использует генерацию кода на основе моделей для автоматического создания классов специализации. Если вы заинтересованы в создании кода с помощью этих инструментов сами, ознакомьтесь с последней версией генератора на GitHub !