На прошлой неделе случился релиз десятки — и хотя Graal был доступен и раньше, теперь он стал ещё доступней — Congratulations, you're running #Graal! — просто добавьте


-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler


Что конкретно это может нам дать и где можно ожидать улучшений, и какие велосипеды надо начинать выпиливать?


Пример, который я буду рассматривать — частично надуманный, однако, основанный на реальных событиях.



Guava


Наверняка многие используют класс Preconditions из библиотеки guava:


checkArgument(value > 0, "Non-negative value is expected, was %s", value);


И всё было бы хорошо, если бы подобный кусок не попадался на критическом пути в коде — проблема в неявном создании мусора.


Так выглядит тело метода checkArgument :


  public static void checkArgument(
      boolean expression,
      @Nullable String errorMessageTemplate,
      @Nullable Object... errorMessageArgs) {
    if (!expression) {
      throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
    }
  }

Сделаем же неявное явным:


boolean expression = value > 0;
Object[] errorMessageArgs = new Object[]{Integer.valueOf(value)};
if (!expression) {
  throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
}

Здесь возникает дялемма шашечки-или ехать: Как правило похожие проверки в production коде это перестаховки, и с одной стороны не хочется за них платить дополнительным мусором, но с другой стороны fast fail не хочется выбрасывать.


Проблема в объектах порождаемых autoboxing и varargs, которые могут быть не использованы. Увы, но сталкиваясь с ветвлением Escape Analysis уже не в состоянии определить объект как ненужый.


Как можно решить проблему?


Например, перегрузив метод checkArgument (что в общем-то и сделано в guava):


  public static void checkArgument(boolean expression, @Nullable String errorMessageTemplate, int p1) {
    if (!expression) {
      throw new IllegalArgumentException(format(errorMessageTemplate, p1));
    }
  }

Но, что если у нас не один аргумент, а больше двух — для которых есть перегруженные методы в guava? Писать свой костыль, либо страдать от мусора? В нашем коде мы столкнулись с местом, которое содержит комбинацию из 3х int, одной строки, которое выполняется миллионы раз и время отклика ограничено.


Graal


Java 10 и -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler


Graal несёт на себе множество новых оптимизаций, и в частности Partial Escape Analysis — суть которого, среди прочего, заключается в том, что он в состоянии определить, что созданные объекты используются только в одном из ветвлении — и можно переместить создание этих объектов внутрь него.


Момент истины — какие ваши доказательства?


@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
@Warmup(iterations = 5, time = 5000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 5000, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class PartialEATest {

    @Param(value = {"-1", "1"})
    private int value;

    @Benchmark
    public void allocate(Blackhole bh) {
        checkArg(bh, value > 0, "expected non-negative value: %s, %s", value, 1000, "A", 700);
    }

    private static void checkArg(Blackhole bh, boolean cond, String msg, Object ... args){
        if (!cond){
            bh.consume(String.format(msg, args));
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(PartialEATest.class.getSimpleName())
                .addProfiler(GCProfiler.class)
                .build();

        new Runner(opt).run();
    }
}

Из всех цифр нас интересуют аллокации — именно поэтому включил GCProfiler :


Options Benchmark (value) Score Error Units
-Graal PartialEATest.allocate:·gc.alloc.rate.norm -1 1008,000 ± 0,001 B/op
-Graal PartialEATest.allocate:·gc.alloc.rate.norm 1 32,000 ± 0,001 B/op
+Graal PartialEATest.allocate:·gc.alloc.rate.norm -1 1024,220 ± 0,908 B/op
+Graal PartialEATest.allocate:·gc.alloc.rate.norm 1 ? 10?? B/op

Что вполне наглядно демонстрирует, что Graal не создает объекты без надобности — и самое время выпиливать оптимизационные костыли.

Комментарии (2)


  1. sunless
    26.03.2018 12:53

    Из всех цифр нас интересуют аллокации — именно поэтому включил Из всех цифр нас интересуют аллокации — именно поэтому включил GCProfiler

    Тут бы рассказать, кто такой GCProfiler и как он работает


    1. vladimir_dolzhenko Автор
      26.03.2018 13:44

      Я полагаю, что люди более-менее знают или владеют JMH — GCProfiler в качестве метрик собирает кол-во аллокаций и кол-во аллоцированных байт, количество сборок и т.п — более подробно разобрано в примере JMHSample 35 Profilers.