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



Java collections


На данный момент в распоряжении мы имеет больше чем достаточно, как родных JDK, так и сторонних библиотек для работы с коллекциями:


JDK


Open Source


Выбор основания


Для создании универсальной коллекции подходит использование принципов generics. Основу коллекции создает класс java.util.AbstractList. Обязательными к переопределению являются методы get и size.


public class FastArray<E> extends AbstractList<E> {
    @Override
    public E get(int index) {
        return null;
    }

    @Override
    public int size() {
        return 0;
    }
}

К тому же, тело некоторых методов абстрактного класса AbstractList определено как throw new UnsupportedOperationException(), что заставляет нас их так же переопределить на нужное нам поведение:


    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

Принцип хранения и работы с данными


Структура хранения данных в коллекции представлена в виде одной матрицы и одного массива c типом IndexData:


    private Object[][] elementData;
    private IndexData[] data;

Для хранения элементов списка используется двухмерный массив elementData. Двухмерный массив в ущерб объему памяти позволяет обеспечивать более быстрый доступ к элементам списка в процессе чтения, записи и удаления быстрые операции вставки/удаления в начало/середину списка.


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


    private static final int CAPACITY = Integer.SIZE;

То есть, все сводится к тому, что при нехватке размера хранилища elementData, будет выполнено:


  1. динамическое выделение памяти на новый размер первой размерности массива
  2. скопированы ссылки на все массивы второй размерности
  3. динамически создан новый массив второй размерности

Массив IndexData требуется для хранения занятых мест и их общего количества во второй размерности elementData:


class IndexData
    private static class IndexData {
        private int data;
        private int size;

        private boolean isFull() {
            return size == CAPACITY;
        }

        private void take(final int index) {
            if (isFree(index)) {
                data = data | 1 << index;
                size++;
            }
        }

        private void free(final int index) {
            if (isTaked(index)) {
                data = data ^ 1 << index;
                size--;
            }
        }

        private boolean isTaked(final int index) {
            return !isFree(index);
        }

        private boolean isFree(final int index) {
            return (data & 1 << index) == 0;
        }

        private int getSize() {
            return size;
        }

        private int getIndexLeadingFreeBit() {
            return CAPACITY - Integer.numberOfLeadingZeros(data);
        }

        private int getIndexTrailingFreeBit() {
            return Integer.numberOfTrailingZeros(data) - 1;
        }
    }

32 места хранятся в битах одного 32-битого числа. Данный прием позволяет сократить некоторые циклы при обходе массивов. Например, поиск свободного места слева или справа во второй размерности.


Но в одном месте все же пришлось использовать цикл
    private int getTakedIndexAt(int data, int number) {

        int count = 0;
        int index = -1;

        number++;

        do {
            count += data & 1;
            index++;
        } while ((data >>>= 1) != 0 && count != number);

        return index;
    }

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


Тесты


Коллекция FastArray начинает показывать положительные результаты после того, как превысит свой размер в 1000 элементов. В противном случае, время на выполнение операций существенно увеличено по сравнению с результатами других коллекций.


Для тестирования использовался следующий алгоритм
        int count = 100000;

        for(int i = 0; i < count; i++)
            list.add(1);

        for(int i = 0; i < count; i++)
            list.add(rand.nextInt(count), 10);

        for(int i = 0; i < count * 2; i++)
            list.get(rand.nextInt(count));

        for(int i = 0; i < count; i++)
            list.remove(rand.nextInt(list.size()));

Результаты тестирования коллекций


# Name Time (ms)
1 FastArray 804
2 HPPC 2857
3 Guava 2887
4 Commons Collections 2909
5 ArrayList 2949
6 PCJ 6370
7 Trove 6503
8 LinkedList 83002

UPD: JMH benchmark








Исходный текст тестов представлен на github


Вывод


Ещё раз хочу заметить, что за уменьшение времени выполнения мы расплачиваемся объемом расходуемой памяти. При выполнении вышеуказанных тестов в процессе дефрагментации размер коллекции FastArray увеличился в 3 раза от номинального. Для уменьшения размера коллекции используется метод trimToSize().
Данная версия реализации не является финальной. Здесь реализован минимально требуемый функционал, который в последствии можно более тщательно оптимизировать.


Исходный текст полностью представлен на github


Благодарю за внимание

Поделиться с друзьями
-->

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


  1. relgames
    20.09.2016 18:17
    +2

    более быстрый доступ к элементам списка в процессе чтения, записи и удаления

    А где тесты на JMH для каждого из случаев? Желательно на разных размерах: 10, 1 000, 1 000 000, 100 000 000
    И для разных случаев: sequential access, random access.


    1. vladimir_dolzhenko
      20.09.2016 19:14
      +2

      — вставка в начало, в конец, в середину
      — удаление элементов в начало, в конец, в середину


    1. Alex_java
      21.09.2016 10:41
      +1

      Добавлены тесты jmh benchmark


      1. relgames
        21.09.2016 11:40

        Спасибо. То есть вот эта фраза из поста «более быстрый доступ к элементам списка в процессе чтения, записи и удаления» на самом деле превращается в «более быстрое добавление/удаление в начале/середине».

        Теперь финт ушами: для этих конкретных задач все равно ArrayList подходит больше, если эти операции выразить через добавление в конец и чтение задом наперед. Например, для добавления в середину можно использовать 2 ArrayList, а при чтении объединять.

        Потому что стандартные коллекции Java предельно оптимизированы, и используют приемы, недоступные обычному программисту, типа JIT оптимизации.


  1. AndreyRubankov
    20.09.2016 18:48
    +3

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

    Как минимум операции доступа и изменения коллекции нужно разбить на разные тесты. Доступ по индексу однозначно будет в разы хуже, чем в ArrayList, т.к. вместо 1 операции взятия элемента из массива, нужно будет сначала из индекса (data) взять данные, и лишь потом из elementData.

    И как советовали выше, используйте JMH для тестов.


  1. vladimir_dolzhenko
    21.09.2016 11:03
    +1

    И, где же iterate? по последовательным и по случайным? И как бы даже из приведённых benchmark-ов — вывод очень неоднозначный


    1. Alex_java
      21.09.2016 11:15

      Данная коллекция изначально не претендовала на замену стандартным коллекциям, а представлена как идея. Плюс, реализован только базовый функционал без оптимизации.


  1. Dionis_mgn
    21.09.2016 11:51

    Возникает ощущение, что Вы пытались придумать std::deque. Думаю, поглядеть его реализацию в GCC/Clang будет полезным.


  1. AndreyRubankov
    21.09.2016 12:17
    +1

    Спасибо за добавленные JMH тесты, выглядит лучше. Но попробуйте поменять единицы измерения на op/sec, это будет нагляднее при данного рода тестах.
    Как вариант, можно еще добавить столбец сравнения с LinkedList (на него будет ориентир при вставках в начало и середину).