Производительность Collections.emptyList и пустого ArrayList с JIT-компилятором

Есть ли разница в производительности между использованием Collections.emptyList() и пустым ArrayList, особенно при использовании JIT-компилятора?

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

Изменить Я знаю, что Collections.emptyList() возвращает неизменяемый список, а ArrayList — изменяемый объект.

Я имею в виду, что если я передаю тот или иной метод в качестве параметра, и метод не изменяет список, ограничивает ли это возможности JIT-компилятора для оптимизации метода?

Простой пример (просто чтобы пояснить, что я имею в виду):

int sum(List<Integer> list)
{
    int sum = 0;

    for(int i=0;i<list.size();++i)
      sum += list.get(i);

    return sum;
}

Если бы я вызывал этот метод только с ArrayList, компилятор JIT мог бы встроить ArrayList.get(). Если бы я также звонил с Collections.empty(), это было бы невозможно.

Это правильно?


person Jimmy R.T.    schedule 24.10.2015    source источник
comment
JIT не заботится о фактическом типе списка, переданного методу. Он даже не знает: вы можете передать пустой список в 10% случаев, ArrayList в 45% случаев, LinkedList в 15% случаев и CopyOnWriteArrayList в остальное время. Почему это помешает ему оптимизировать метод?   -  person JB Nizet    schedule 24.10.2015
comment
@JBNizet Компилятор JIT знает загруженный байт-код и, следовательно, может знать, получает ли метод только один тип объектов. Мой сценарий передает только ArrayList по сравнению с передачей ArrayList и пустым списком. Вы правы в остальном.   -  person Jimmy R.T.    schedule 24.10.2015
comment
Итак, вы спрашиваете, будет ли разница для JIT между методом, объявленным как foo(List<Bar> list) и foo(ArrayList<Bar> list), верно? Если нет, уточните. Почтовый индекс. Но в любом случае, сделайте свой дизайн чистым, вместо того, чтобы пытаться преждевременно оптимизировать то, что в любом случае не нужно оптимизировать. Java использует пулипоризм, и JIT может с этим справиться. Не беспокойтесь об этом.   -  person JB Nizet    schedule 24.10.2015
comment
@JBNizet Однажды я читал, что JIT-компилятор создает другой код, если для интерфейса существует только одна реализация. Я думаю, что это может быть применимо и здесь. Кстати, я не использую это для оптимизации, я просто пытаюсь понять JIT-компилятор.   -  person Jimmy R.T.    schedule 24.10.2015
comment
Но существует очень много реализаций List, так что здесь он явно не применим.   -  person JB Nizet    schedule 24.10.2015
comment
@JBNizet Я знаю, что это не то же самое. Я бы не задавал вопрос, если бы думал так.   -  person Jimmy R.T.    schedule 24.10.2015


Ответы (3)


Отказ от ответственности

Все написанное ниже относится только к JVM HotSpot.

Короткие ответы

компилятор JIT не выполняет встроенные или статические вызовы методов, поскольку выполняемый метод зависит от типа.

Это противоположно истине. Смотрите мой ответ.

Есть ли разница в производительности между использованием Collections.emptyList() или пустым ArrayList, особенно при использовании JIT-компилятора?

В редких случаях - да. См. результаты микробенчмарка.

Если бы я вызывал этот метод только с ArrayList, компилятор JIT мог бы встроить ArrayList.get(). Если бы я также делал вызовы с Collections.empty(), это было бы невозможно. Это правильно?

Короткий ответ - это зависит. Компилятор JIT достаточно умен, чтобы распознавать мономорфные, биморфные и полиморфные шаблоны вызовов и обеспечивать соответствующие реализации.

Отвечать

Чтобы получить развернутый ответ, рекомендую прочитать следующий пост о черной магии диспетчеризации методов. В нескольких словах

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

Рассмотрим следующий пример JMH (если вы не знали о JMH, то предлагаю прочитать о нем здесь).

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 5)
public class ExampleBench {

    @Param("10000")
    private int count;

    List<Integer>[] arrays;
    List<Integer>[] empty;
    List<Integer>[] bimorphic;
    List<Integer>[] polimorphic;

    @Setup
    public void setup(){
        Random r = new Random(0xBAD_BEEF);
        arrays = new List[count];
        empty = new List[count];
        bimorphic = new List[count];
        polimorphic = new List[count];
        for (int i = 0; i < arrays.length; i++) {
            bimorphic[i] = r.nextBoolean() ? new ArrayList<Integer>(0) : Collections.<Integer>emptyList();
            int i1 = r.nextInt(3);
            switch (i1) {
                case 0 : polimorphic[i] = new ArrayList<>(0);
                    break;
                case 1 : polimorphic[i] = new LinkedList<>();
                    break;
                case 2 : polimorphic[i] = Collections.emptyList();
                    break;
            }
            arrays[i] = new ArrayList<>(0);
            empty[i] = Collections.emptyList();
        }
    }

    @Benchmark
    public float arrayList() {
        List<Integer>[] l = arrays;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float emptyList() {
        List<Integer>[] l = empty;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float biList() {
        List<Integer>[] l = bimorphic;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    @Benchmark
    public float polyList() {
        List<Integer>[] l = polimorphic;
        int c = count;
        float result = 0;
        for (int i = 0; i < c; i++) {
            result += sum(l[i]);
        }
        return result;
    }

    int sum(List<Integer> list) {
        int sum = 0;
        for (int i = 0; i < list.size(); ++i) {
            sum += list.get(i);
        }
        return sum;
    }
}

Результаты:

Benchmark               (count)  Mode  Cnt       Score       Error  Units
ExampleBench.arrayList    10000  avgt    5   22902.547 ± 27665.651  ns/op
ExampleBench.biList       10000  avgt    5   50459.552 ±   739.379  ns/op
ExampleBench.emptyList    10000  avgt    5    3745.469 ±   211.794  ns/op
ExampleBench.polyList     10000  avgt    5  164879.943 ±  5830.008  ns/op

В случае мономорфных и биморфных вызовов JIT заменяет виртуальный вызов конкретными реализациями. Например, в случае arrayList() у нас есть следующий вывод для -XX:+PrintInlining:

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.ArrayList::size (5 bytes)   accessor
    \-> TypeProfile (15648/15648 counts) = java/util/ArrayList

для emptyList():

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.Collections$EmptyList::size (2 bytes)   inline (hot)
    \-> TypeProfile (9913/9913 counts) = java/util/Collections$EmptyList

для biList():

@ 27   edu.jvm.runtime.ExampleBench::sum (38 bytes)   inline (hot)
   @ 6   java.util.Collections$EmptyList::size (2 bytes)   inline (hot)
   @ 6   java.util.ArrayList::size (5 bytes)   accessor
    \-> TypeProfile (2513/5120 counts) = java/util/ArrayList
    \-> TypeProfile (2607/5120 counts) = java/util/Collections$EmptyList

В случае polyList() JIT не встраивает какую-либо реализацию и использует настоящий виртуальный вызов.

Каковы преимущества использования встроенных функций в этих методах? Давайте посмотрим на сгенерированный компилятором код для arrayList():

0x00007ff9e51bce50: cmp $0xf80036dc,%r10d     ;instance of 'java/util/ArrayList'
0x00007ff9e51bce57: jne L0000                 ;if false go to L0000 (invokeinterface size)
0x00007ff9e51bce59: mov 0x10(%rdx),%ebp       ;*getfield size optimization java.util.ArrayList::size@1 

.....

0x00007ff9e51bce6d: retq
             L0000: mov $0xffffffde,%esi      ; true virtual call starts here
0x00007ff9e51bce73: mov %rdx,(%rsp)
0x00007ff9e51bce77: callq 0x00007ff9e50051a0  ; OopMap{[0]=Oop off=92}
                                              ;*invokeinterface size
                                              ; - edu.jvm.runtime.ExampleBench::sum@6 (line 119)
                                              ;   {runtime_call}

Как видите, JIT заменяет виртуальный вызов на getfield.

person Ivan Mamontov    schedule 26.10.2015
comment
Хорошая работа. Сравнение будет более актуальным, если вы добавите в списки некоторые элементы. Например, оставить только массивы и биморфные тесты, в биморфном заменить непустые ArrayLists на ArrayLists, содержащие некоторые элементы, и случайным образом чередовать пустые и непустые списки в массивах. Таким образом, вы можете понять, как это замедляет более реальный сценарий... - person Tagir Valeev; 27.10.2015

Collections.emptyList() всегда возвращает один и тот же неизменный пустой объект списка (одноэлементный). С другой стороны, создание ArrayList фактически создает новый объект, выделяет память, и этот объект должен быть проверен позже.

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

person JB Nizet    schedule 24.10.2015

Collections.emptyList() и пустой new ArrayList<>() работают немного по-разному. Список, возвращаемый Collections, не только пуст, он неизменяем, так что сам список может храниться как синглтон и возвращаться при каждом вызове emptyList() (благодаря стиранию типа, это возможно для любого типа квалификаторы).

Итак, ответ зависит от того, что вы собираетесь делать с пустым списком. Если вы собираетесь возвращать пустой список в каком-то своем коде в качестве окончательного значения, Collections.emptyList(), безусловно, лучше (он даже не создает новый объект). Если вы собираетесь настроить пустой список для дальнейших модификаций, Collections.emptyList() совершенно недопустимо

person Vasily Liaskovsky    schedule 24.10.2015