Java: каковы издержки использования ConcurrentSkipList *, когда не требуется параллелизм? - PullRequest
4 голосов
/ 03 ноября 2011

Мне нужен отсортированный список в сценарии, в котором преобладает итерация (по сравнению со вставкой / удалением, а не случайным получением).По этой причине я думал об использовании списка пропусков по сравнению с деревом (итератор должен быть быстрее).

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

Насколько я знаю, ConcurrentSkipList * в основном являются реализациями без блокировок, основанными на CAS, поэтому они не должны нести (много)накладные расходы, но я хотел бы услышать мнение другого.

РЕДАКТИРОВАТЬ:

После некоторого микро-бенчмаркинга (многократный запуск итерации на TreeSet разного размера, LinkedList, ConcurrentSkipList и ArrayList) показывает, что естьдовольно накладные расходы.ConcurrentSkipList хранит элементы в связанном списке внутри, поэтому единственная причина, по которой он будет медленнее на итерации, чем LinkedList, связана с вышеупомянутыми издержками.

Ответы [ 6 ]

3 голосов
/ 10 ноября 2011

При измерении с использованием Open JDK 6 на типичном производственном оборудовании, используемом моей компанией, можно ожидать, что все операции добавления и запроса на карте с пропущенным списком будут занимать примерно удвоенное время, что и та же операция на дереве карта.

Примеры:

63 usec против 31 usec для создания и добавления 200 записей. 145 нс против 77 нс для get () на этой карте из 200 элементов.

И соотношение не так уж и сильно меняется для маленьких и больших размеров.

(Код для этого эталонного теста в конечном итоге будет передан, так что вы можете просмотреть его и запустить его самостоятельно; извините, мы еще не готовы сделать это.)

3 голосов
/ 05 ноября 2011

Если безопасность потока не требуется, я бы сказал, чтобы вообще пропустить package java.util.concurrent.

Интересно то, что иногда ConcurrentSkipList медленнее, чем TreeSet на том же входе, и я не разобралсяно почему.

Я имею в виду, вы видели исходный код ConcurrentSkipListMap?:-) Мне всегда приходится улыбаться, когда я смотрю на это.Это 3000 строк одного из самых безумных, страшных и в то же время прекрасных кодов, которые я когда-либо видел в Java.(Спасибо Doug Lea и соавторам за то, что все утилиты параллелизма так хорошо интегрированы с платформой коллекций!) Сказав, что на современных процессорах код и алгоритмическая сложность даже не будут иметь большого значения.Что обычно имеет большее значение, так это наличие данных для итерации, расположенных в памяти, чтобы кэш ЦП мог лучше выполнять свою работу.

Так что в конце я оберну ArrayList новым addSorted() метод, который выполняет сортировку вставки в ArrayList.

Звучит хорошо.Если вам действительно нужно выжать каждую каплю производительности из итерации, вы также можете попробовать выполнить итерацию необработанного массива напрямую.Повторно заполняйте его при каждом изменении, например, вызывая TreeSet.toArray() или генерируя его, а затем сортируя на месте, используя Arrays.sort(T[], Comparator<? super T>).Но выигрыш может быть крошечным (или даже ничем, если JIT хорошо выполняет свою работу), поэтому он может не стоить неудобств.

1 голос
/ 03 ноября 2011

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

Я начал с создания простого Iterator<String>, которое бесконечно зацикливается на конечном списке случайно сгенерированных строк. (То есть: при инициализации он генерирует массив _strings из a случайных строк длиной b из пула c различных символов. Первый вызов next() возвращает _strings[0], следующий вызов возвращает _strings[1] & hellip; ( n + 1) -й вызов снова возвращает _strings[0].) Строки, возвращенные этим итератором, были тем, что я использовал во всех звонках SortedSet<String>.add(...) и SortedSet<String>remove(...).

Затем я написал тестовый метод, который принимает пустой SortedSet<String> и зацикливается d раз. На каждой итерации добавляется e элементов, затем удаляется f элементов, а затем выполняется итерация по всему набору. (В качестве проверки работоспособности он отслеживает размер набора, используя возвращаемые значения add() и remove(), и когда выполняет итерацию по всему набору, он удостоверяется, что находит ожидаемое количество элементов. В основном я сделал просто 1030 * что-то в теле цикла.)

Не думаю, что мне нужно объяснять, что делает мой метод main(...). : -)

Я пробовал различные значения для различных параметров, и я обнаружил, что иногда ConcurrentSkipListSet<String> работал лучше, а иногда TreeSet<String>, но разница была не более чем в два раза. В целом, ConcurrentSkipListSet<String> работает лучше, когда:

  • a , b и / или c были относительно большими. (Я имею в виду, в диапазоне, который я тестировал. Мои a были в диапазоне от 1000 до 10000, мои b - от 3 до 10, мои c ' s от 10 до 80. В целом, размеры результирующих наборов варьировались от 450 до 10000, с режимами 666 и 6666, потому что я обычно использовал e = 2 & lrm; f .) Это говорит о том, что ConcurrentSkipListSet<String> справляется несколько лучше, чем TreeSet<String> с большими наборами и / или с более дорогими сравнениями строк. Пробуя конкретные значения, разработанные для того, чтобы разделить эти два фактора, у меня сложилось впечатление, что ConcurrentSkipListSet<String> справляется заметно лучше, чем TreeSet<String> с большими наборами, и немного меньше , что хорошо при более дорогих сравнениях строк. (Это в основном то, что вы ожидаете; подход бинарного дерева TreeSet<String> направлен на то, чтобы сделать абсолютно минимальное возможное количество сравнений.)
  • e и f были маленькими; то есть, когда я вызывал add(...) s и remove(...) s только небольшое количество раз за итерацию. (Это именно то, что вы предсказали.) Точная точка оборота зависела от a , b и c , но в первом приближении ConcurrentSkipListSet<String> показали лучшие результаты, когда e + f было меньше, чем около 10, а TreeSet<String> показали лучшие результаты, когда e + f было больше около 20.

Конечно, это было на машине, которая может выглядеть совсем не так, как ваша, с использованием JDK, который может выглядеть совсем не так, как ваш, и с использованием очень искусственных данных, которые могут выглядеть совсем не так, как ваша. Я бы порекомендовал вам запустить свои собственные тесты. Так как Tree* и ConcurrentSkipList* оба реализуют Sorted*, у вас не должно возникнуть никаких проблем, пробуя свой код в обоих направлениях и видя то, что вы найдете.

Насколько я знаю, ConcurrentSkipList * в основном являются реализациями без блокировок, основанными на CAS, поэтому они не должны нести (много) накладных расходов, [& hellip;]

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

1 голос
/ 03 ноября 2011

Без параллелизма обычно более эффективно использовать сбалансированное двоичное дерево поиска.В Java это будет TreeMap.

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

1 голос
/ 03 ноября 2011

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

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

Сложностьпри итерации списка дерева или списка пропусков всегда будет O (n), но если вместо этого вы используете связанный список или список массивов, у вас возникает проблема со вставкой: вставить элемент в правильную позицию (отсортированный связанный список)сложность вставки будет O (n) вместо O (log n) для двоичного дерева или для списка пропуска.

Вы можете перебрать TreeMap.keySet (), чтобы получить все вставленные ключи в порядке иэто не будет так медленно.

Существует также класс TreeSet, это, вероятно, то, что вам нужно, но так какэто просто оболочка для TreeMap, прямое использование TreeMap будет быстрее.

0 голосов
/ 15 ноября 2017

Как уже отмечалось, SkipList имеет много накладных расходов по сравнению с TreeMap, и итератор TreeMap не очень подходит для вашего случая использования, потому что он просто многократно вызывает метод successor (), который оказывается очень медленным.
Так что одинальтернатива, которая будет значительно быстрее двух предыдущих, - это написать собственный итератор TreeMap.На самом деле, я бы вообще сбросил TreeMap, так как 3000 строк кода немного громоздче, чем вам, вероятно, нужно, и просто написал бы чистую реализацию дерева AVL с необходимыми вам методами.Основная логика AVL - это всего лишь несколько сотен строк кода на любом языке , затем добавьте итератор, который лучше всего работает в вашем случае.

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