Unrolled linked list на Java

в 17:43, , рубрики: algorithms, data structures, java, list, Алгоритмы, Песочница, метки: , , ,

Всем привет.

Время от времени я, как и большинство программистов, изучаю какие-то новые структуры и алгоритмы.

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

Одним из представителей этой группы является Unrolled linked list.

Что же это такое?

Чтобы понять, что же это такое и с чем его едят, предлагаю вспомнить наиболее популярные реализации списков: ArrayList (с некоторым упрощением будем говорить о массивах вместо непосредственно списка) и LinkedList.

Рассмотрим их основные достоинства и недостатки:

ArrayList:

Достоинства

  • Компактность — массив является наиболее (по крайней мере одной из) компактной структур данных для представления списков;
  • Скорость вставки в конец незаполненного массива — операция очень быстрая, факт!;
  • Скорость взятия элемента по индексу — также очень высока, количество cache miss можно оценить в N/B, где B — размер строки кэша, а N — количество элементов массива, что можно считать эталонным для такого рода структуры.

Недостатки

  • Вставка в середину — не очень удобная операция для массива, приходится сдвигать правую часть, что даже с arraycopy всё же не бесплатно;
  • Протяжённость области памяти — для хранения больших массивов требуется большой непрерывный участок памяти. И хотя многие сборщики мусора эффективно борются с фрагментацией памяти, вероятность OutOfMemmory особенно для больших списков является существенной.

LinkedList:

Достоинства

  • Относительно более простая и быстрая операция вставки в середину;
  • Нет проблем при работе с сильно фрагментированой памятью.

Недостатки

  • Существенно больший объём необходимой памяти для хранения данных того же размера (до 4-х раз);
  • При итерации по списку количество cache miss в худшем случае может быть равно количеству нод списка. Даже в лучшем случае, когда все ноды находятся в пямяти друг за другом, за счёт большего размера ноды количество cache miss будет в несколько раз больше такового для ArrayList.

Так вот UnrolledLinkedList пытается объединить достоинства двух списков и при этом не унаследовать их недостатки.
Рассмотрим устройство этого списка более подробно.

Попросту говоря, UnrolledLinkedList — связный(двусвязный) список массивов длины n. То есть каждая нода содержит в себе не одно значение, а массив значений. При этом массив является достаточно маленьким, чтобы опреции вставки и удаления были очень быстрыми и фрагментация памяти меньше влияла на возможность создания ноды, но достаточно большие, чтобы полностью помещаться в строку кэша, для сокращения количества cache miss. Дополнительно каждая нода хранит в себе количество добаленных в неё элементов. Графически этот список можно представить следующим образом:

image

Рассмотрим оперции для этого списка:

Вставка в ноду:
В случае наличия свободных ячеек, новый элемент просто добавляется в конец массива. Если массив уже полон, то мы создаём новую ноду за текущей и переносим в неё правую половину массива, создавая тем самым место для вставки новых значений. Новый элемент в таком случае будет вставлен в конец новой ноды. То же можно применить к вставке в середину массива.

Графически эту операцию можно представить следующим образом:

image

Удаление из ноды:
Операция досточно простая, мы просто удаляем нужный элемент из массива и делаем сдвиг остальных элементов, если это необходимо. Но есть одно но, в случае если количество элементов в массиве уменьшается до n/2, то из соседних ячеек переносятся n/2 элементов. Если после этого какая-то из ячеек оказалась пустой — она удаляется. Эта опреция нужна для редуцирования расхода памяти при постепенной очистке больших списков.

Поиск ноды по индексу:
Как и связном списке поиск начинается с головы иди хвоста, откуда ближе, отличие в том, что итерироваться по массивам нет нужды — необходимо просто суммировать значения полей содержащие количество элементов в ноде. Таким образом поиск нужной ноды происходит существенно быстрее, чем в LinkedList, хотя асимптотика тут попрежнему O(N).

Изначально мне показалось, что половина пустых ячеек в каждой ноде — это слишком расточительно. Однако как показали небольшие экперименты, в среднем уровень заполнения ячеек находится на уровне 70%, и даже с 50% заполнением расход памяти должен быть существенно меньше, чем у связного списка. Более того список позволяет настраивать себя под различные нужды. Я пытался поиграть с коэффициентом заполнения массива, но существенной выгоды не извлёк, поэтому оставил всё как есть.

Говоря про количество cache miss, то при итерации по таком списку его можно оценить как (N/n+1)(n/B), где N — длина списка, n — длина массива ноды, B — длина строки кэша. Такая оценка достаточно близка к оценке для ArrayList.

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

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

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

Спасибо за внимание.

Источники:
blogs.msdn.com/b/devdev/archive/2005/08/22/454887.aspx
en.wikipedia.org/wiki/Unrolled_linked_list

Автор: Falland

  1. www888com:

    В классе ListItr в методе set(E e) разве не next.items[next.elementOffset-1]=e? Иначе при Collections.sort(list) выскакивает IndexOfBoundException

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js