Пропустити список проти дерев бінарного пошуку


218

Нещодавно я натрапив на структуру даних, відому як пропускний список . Схоже, це дуже схожа поведінка з двійковим деревом пошуку.

Чому ви коли-небудь хочете використовувати список пропусків над двійковим деревом пошуку?

Відповіді:


257

Пропускні списки піддаються одночасному доступу / зміні. Герб Саттер написав статтю про структуру даних у одночасних середовищах. У ньому більше невмілої інформації.

Найбільш часто втілене в життя двійкове дерево пошуку - це червоно-чорне дерево . Попутні проблеми виникають, коли дерево модифіковане, воно часто потребує відновлення. Операція відновлення балансу може впливати на великі ділянки дерева, що вимагатиме блокування мютексу на багатьох деревних вузлах. Вставлення вузла до списку пропусків набагато локалізованіше, потрібно лише заблокувати лише вузли, безпосередньо пов’язані з ураженим вузлом.


Оновлення від коментарів Джона Харропса

Я читав останню статтю Фрейзера та Харріса Паралельне програмування без замків . Дійсно хороший матеріал, якщо вам цікаві безблокові структури даних. Документ зосереджується на транзакційній пам'яті та теоретичній операції Multiword-зіставлення та заміна MCAS. Обидва вони моделюються в програмному забезпеченні, оскільки жодне обладнання їх не підтримує. Мені досить вражено, що вони взагалі змогли створити MCAS в програмному забезпеченні.

Я не вважав речі з оперативної пам'яті особливо переконливими, оскільки це вимагає збору сміття. Крім того, транзакційна пам'ять програмного забезпечення зазнає проблем з продуктивністю. Однак я дуже буду схвильований, якщо пам'ять про транзакційну техніку коли-небудь стане загальним. Зрештою, це все ще дослідження і не буде корисним для виробничого коду ще близько десяти років.

У розділі 8.2 вони порівнюють ефективність кількох одночасних реалізацій дерев. Я підсумую їхні висновки. Варто завантажити pdf, оскільки він має дуже інформативні графіки на сторінках 50, 53 та 54.

  • Блокування пропускних списків шалено швидко. Вони неймовірно добре масштабуються за кількістю одночасних звернень. Саме це робить пропускні списки спеціальними, інші структури даних на основі блокування, як правило, стискаються під тиском.
  • Списки пропуску без блокування постійно швидше, ніж блокування списків пропуску, але лише ледь.
  • списки пропуску трансакцій послідовно в 2-3 рази повільніше, ніж версії блокування та незамикання.
  • замикаючи червоно-чорні дерева, що просідають під одночасним доступом. Їх продуктивність погіршується лінійно з кожним новим одночасно користувачем. З двох відомих реалізацій блокування червоно-чорних дерев, одна, по суті, має глобальний замок під час відновлення дерев. Інший використовує фантазійну (і складну) ескалацію блокування, але все ще не дає значної версії глобального блокування.
  • червоно-чорні дерева без замків не існують (більше неправда, див. Оновлення).
  • трансакційні червоно-чорні дерева порівнянні з транзакційними пропускними списками. Це було дуже дивно і дуже перспективно. Транзакційна пам'ять, хоча повільніше, якщо набагато простіше писати. Це може бути настільки ж просто, як швидкий пошук та заміна на несучасній версії.

Оновлення
Ось документ про дерева без замків : Червоно-чорні дерева без замка за допомогою CAS .
Я не вдивлявся в нього глибоко, але на поверхні це здається солідним.


3
Не кажучи вже про те, що в невиродженому списку пропуску приблизно 50% вузлів повинні мати лише єдине посилання, яке робить вставку та видалення надзвичайно ефективними.
Адісак

2
Повторне врівноваження не вимагає блокування мютексу. Дивіться cl.cam.ac.uk/research/srg/netos/lock-free
Джон Харроп

3
@Jon, так і ні. Немає відомих реалізацій червоного-чорного дерева без замків. Фрейзер і Харріс показують, як реалізовано червоно-чорне дерево на основі трансакційної пам'яті та її ефективність. Трансакційна пам’ять ще дуже велика на дослідницькій арені, тому у виробничому коді червоно-чорному дереву все одно знадобиться зафіксувати великі частини дерева.
deft_code

4
@deft_code: нещодавно Intel оголосила про реалізацію транзакційної пам'яті через TSX на Haswell. Це може виявитись цікавим для згаданих вами структур безвісних блокувань.
Майк Бейлі

2
Я думаю , що відповідь Фізза є більш актуальною (з 2015 року), а не цією відповіддю (2012 р.), І тому, мабуть, слід вважати переважну відповідь на сьогоднішній день.
fnl

81

По-перше, ви не можете справедливо порівняти рандомізовану структуру даних із тією, яка дає найгірші гарантії.

Список пропусків еквівалентний дереву двійкових пошуків (RBST), який є випадковим чином збалансованим, таким чином, який детальніше пояснюється в "Діна та Джонса" "Дослідження подвійності між списками пропусків та бінарними деревами пошуку" .

І навпаки, ви також можете мати детерміновані списки пропусків, які гарантують найгірші результати роботи, пор. Munro та ін.

На відміну від вищезазначених, ви можете мати реалізацію бінарних дерев пошуку (BST), які добре працюють в одночасному програмуванні. Потенційна проблема з BST, орієнтованою на сумісність, полягає в тому, що ви не можете легко отримати те саме, що було гарантіями щодо балансування, як і з червоно-чорного дерева (RB). (Але "стандартні", тобто рандомізовані, пропускні списки також не дають вам цих гарантій.) Існує компроміс між підтримкою балансування в усі часи і хорошим (і простим у запрограмуванні) одночасним доступом, тому зазвичай використовуються розслаблені дерева РБ коли бажана хороша паралельність. Релаксація полягає в тому, щоб не врівноважити дерево відразу. Для дещо датованого (1998 р.) Опитування див. "Виконання паралельних алгоритмів червоно-чорного дерева" Ханке [ps.gz] .

Одне з останніх вдосконалень щодо цього - так зване хроматичне дерево (в основному у вас є така вага, щоб чорний був рівний 1, а червоний - нуль, але ви також допускаєте значення між ними). І як хроматичне дерево протиставляється списку? Подивимось, що Браун та ін. "Загальна техніка неблокуючих дерев" (2014) повинна сказати:

З 128 потоків наш алгоритм перевершує неблокуючий список пропуску Java на 13% до 156%, дерево AVL на основі блокування Bronson та ін. на 63% до 224% та RBT, який використовує транзакційну пам'ять програмного забезпечення (STM) в 13-134 разів

EDIT додати: список пропусків на основі блокування П'ю, який було визначено у «Фейзер та Харріс» (2007) «Паралельне програмування без блокування» , наближається до власної версії без блокування (точка, на яку чітко наполягають у верхній відповіді тут), також налаштовано на хорошу одночасну роботу, пор. П'ю "Постійне обслуговування пропущених списків" П'ю , хоча і досить м'яким чином. Тим не менш, один новий документ / 2009 "Простий оптимістичний алгоритм пропуску"Герліхі та ін., який пропонує нібито простішу реалізацію паралельних списків пропуску (ніж П'ю), критикуючи П'ю за те, що вони не надавали докази коректності, достатнього для них. Залишаючи осторонь цю (можливо, занадто педантичну) приналежність, Герліхій та ін. показують, що їхня простіша реалізація списку пропусків на основі блокування насправді не дає змоги змінити масштаб, а також безблокова реалізація JDK, але лише для високої суперечки (50% вставок, 50% видалення та 0% пошуку) ... які Fraser а Гарріс взагалі не тестувався; Фрейзер і Харріс протестували лише 75% пошуку, 12,5% вставок і 12,5% видалень (у списку пропуску з елементами ~ 500K). Простіша реалізація Herlihy та ін. також наближається до безблокового рішення від JDK у випадку низької суперечності, яку вони перевірили (70% пошуку, 20% вставок, 10% видалень); вони фактично обіграли рішення, що не має блокування для цього сценарію, коли вони зробили свій список пропусків досить великим, тобто перейшли від 200K до 2M елементів, так що ймовірність суперечки на будь-якому блокуванні стала незначною. Було б добре, якби Herlihy та ін. перебралися до довіри П'ю і перевірили його реалізацію, але, на жаль, вони цього не зробили.

EDIT2: Я знайшов материнську область (опублікована 2015 р.) Усіх орієнтирів: Грамолі "Більше, ніж ти хотів знати про синхронізацію. Синхроніч, вимірювання впливу синхронізації на паралельних алгоритмах" : Ось витягнутий образ, відповідний цьому питанню.

введіть тут опис зображення

"Algo.4" - попередник (старший, версія 2011 року) згаданих вище Браун та ін. (Я не знаю, наскільки краща чи гірша версія 2014 року). "Алго.26" - згадуваний вище Герліхій; як ви бачите, він оновлюється в оновленнях і набагато гірше на використовуваних тут процесорах Intel, ніж на процесорах Sun від оригінального паперу. "Algo.28" - це ConcurrentSkipListMap від JDK; це не так добре, як можна було сподіватися порівняно з іншими реалізаціями списку пропусків на основі CAS. Переможцями в умовах високої суперечки є алгоритм на основі блокування (!!), описаний Crain et al. у "Дереві пошуку бінарного пошуку, що сприймає суперечки" та "Algo.30", є "обертовим списком" з "Логарифмічних структур даних для багатоядерних" . ". Зауважте, що Грамолі є співавтором всіх трьох цих статей-алгоритмів-переможців. "Algo.27" - це реалізація C ++ списку пропусків Фрейзера.

Висновок Грамолі полягає в тому, що набагато простіше розробити сумісну реалізацію дерева на основі CAS, ніж створити подібний список пропусків. І виходячи з цифр, важко не погодитися. Його пояснення цьому факту є:

Складність в проектуванні дерева, що не має замків, випливає з труднощів модифікації декількох посилань атомним шляхом. Пропускні списки складаються з башт, пов'язаних між собою через наступні покажчики, і в яких кожен вузол вказує на вузол безпосередньо під ним. Їх часто вважають схожими з деревами, оскільки кожен вузол має спадкоємця в вежі-спадкоємці та під ним, однак, головне відмінність полягає в тому, що нижній покажчик, як правило, незмінний, отже, спрощуючи атомну модифікацію вузла. Ця відмінність, ймовірно, є причиною того, що пропуск перераховує перевершення дерев, що знаходяться в сильному конфлікті, як це спостерігається на малюнку [вище].

Перевага цієї складності була ключовою проблемою в останніх роботах Браун та ін. У них є ціла окрема (2013 р.) Стаття "Прагматичні примітиви для неблокуючих структур даних" про побудову багатозаписних складових LL / SC "примітивів", які вони називають LLX / SCX, які самі реалізуються за допомогою (машинного рівня) CAS. Браун та ін. використовували цей будівельний блок LLX / SCX у 2014 році (але не у 2011 році) паралельній реалізації дерева.

Я думаю, що, мабуть, варто також узагальнити тут основні ідеї пропуску списку "не гаряча точка" / сприятливого до конфлікту (CF). Він додає важливу ідею від розслаблених дерев RB (і подібних чітко визначених структур даних): вежі вже не будуються відразу після вставки, а затримуються, поки не буде менше суперечок. І навпаки, видалення високої вежі може створити багато суперечок; це спостерігалося ще в одночасному папері П'ю про перелік списків 1990 року, тому П'ю запровадив обернення покажчика при видаленні (наголос, про те, що сторінка Вікіпедії у списках пропуску досі не згадується, на жаль). Список пропусків CF робить цей крок далі і затримує видалення верхніх рівнів високої вежі. Обидва типи затримок операцій у списках пропуску CF здійснюються окремою (на основі CAS) ниткою, схожою на збирання сміття, яку її автори називають "адаптаційною ниткою".

Код Synchrobench (включаючи всі перевірені алгоритми) доступний за посиланням: https://github.com/gramoli/synchrobench . Останні Браун та ін. реалізація (не включена у вищезазначене) доступна за адресою http://www.cs.toronto.edu/~tabrown/chromatic/ConcurrentChromaticTreeMap.java У когось доступна основна машина 32+? J / K Моя думка полягає в тому, що ви можете самі це запустити.


12

Крім того, на додаток до наданих відповідей (простота реалізації у поєднанні із порівнянною продуктивністю з збалансованим деревом). Я вважаю, що реалізувати обхід замовлення (вперед і назад) набагато простіше, оскільки список пропусків фактично має пов'язаний список всередині його реалізації.


1
Чи не прохід по порядку для дерева бін є таким простим, як: "def func (node): func (зліва (вузол)); op (node); func (right (node))"?
Клавдіу

6
Впевнені, що це правда, якщо ви хочете пройти все за один виклик функції. але це стає набагато більше дратує, якщо ви хочете пройти ітераторський стиль переходу, як у std :: map.
Еван Теран

@Evan: Не функціональною мовою, де можна просто писати в CPS.
Джон Харроп

@Evan def iterate(node): for child in iterate(left(node)): yield child; yield node; for child in iterate(right(node)): yield child;:? =). не локальний контроль iz awesom .. @Jon: писати в CPS - це біль, але, можливо, ти маєш на увазі продовження? генератори - це в основному особливий випадок продовження роботи пітона.
Клавдіу

1
@Evan: так, він працює до тих пір, поки параметр вузла буде вирізаний з дерева під час модифікації. Траверса C ++ має таке ж обмеження.
deft_code

10

На практиці я виявив, що ефективність B-дерева в моїх проектах виявилася кращою, ніж пропускні списки. Пропустити списки, здається легше зрозуміти , але реалізація B-дерева не що важко.

Єдиною перевагою, про яку я знаю, є те, що деякі розумні люди розробили, як реалізувати одночасний список пропусків без блокування, який використовує лише атомні операції. Наприклад, Java 6 містить клас ConcurrentSkipListMap, і ви можете прочитати до нього вихідний код, якщо ви божевільні.

Але написати паралельний варіант B-дерева теж не важко - я бачив, що це робив хтось інший, - якщо ви попередньо розщеплюєте і зливаєте вузли "про всяк випадок", коли ви спускаєтесь по дереву, тоді вам не доведеться турбуйтеся про тупикові місця і лише коли-небудь потрібно тримати замок на двох рівнях дерева одночасно. Накладні витрати синхронізації будуть трохи вище, але B-дерево, ймовірно, швидше.


4
Думаю, не варто називати Бінарне дерево Б-деревом, є зовсім інший DS з таким ім'ям
Шихаб Шахріар Хан

8

Із статті, яку ви цитували у Вікіпедії :

Θ (n) операції, які змушують нас відвідувати кожен вузол у порядку зростання (наприклад, друк всього списку), надають можливість оптимально виконати закулісну дерандомізацію рівня рівня структури пропускного списку, приведення списку пропусків до часу пошуку O (log n). [...] Пропускний список, за яким ми нещодавно не виконували [жодних подібних] Θ (n) операцій, не дає таких самих абсолютних гарантій ефективності, як у традиційних збалансованих структурах даних про дерево , тому що це завжди можливо (хоча з дуже низькою ймовірністю), що монети, обернені для складання списку пропусків, дадуть погано збалансовану структуру

EDIT: отже, це компроміс: пропуск списків використовує менше пам’яті, ризикуючи перерости їх у неврівноважене дерево.


це буде причиною проти використання списку пропусків.
Клавдіу

7
Цитуючи MSDN, "Шанси [для 100 елементів 1 рівня] є рівно 1 на 1,267,650,600,228,229,401,496,703,205,376".
peterchen

8
Чому б ви сказали, що вони використовують менше пам'яті?
Джонатан

1
@peterchen: Бачу, дякую. Так це не відбувається з детермінованими списками пропусків? @Mitch: "Пропущені списки використовують менше пам'яті". Як пропускні списки використовують менше пам'яті, ніж збалансовані двійкові дерева? Схоже, у них є 4 вказівника у кожному вузлі та дублюючі вузли, тоді як у дерев є лише 2 вказівника і жодних дублікатів.
Джон Харроп

1
@Jon Harrop: Вузлам на рівні першого потрібен лише один покажчик на вузол. Будь-яким вузлам на більш високих рівнях потрібні лише два покажчики на вузол (один на наступний вузол і один на рівень під ним), хоча, звичайно, вузол рівня 3 означає, що ви використовуєте 5 покажчиків для цього одного значення. Звичайно, це все одно висмоктує багато пам’яті (більше того, ніж двійковий пошук, якщо ви хочете непридатний пропуск списку і маєте великий набір даних) ... але я думаю, мені щось не вистачає ...
Брайан

2

Пропускні списки реалізуються за допомогою списків.

Рішення без блокування існують для списків, що мають однозначний і подвійний зв’язок, але немає рішень без блокування, які безпосередньо використовують лише CAS для будь-якої структури даних O (logn).

Однак ви можете використовувати списки на основі CAS для створення пропускних списків.

(Зверніть увагу, що MCAS, створений за допомогою CAS, дозволяє довільні структури даних, а доказ концепції червоно-чорного дерева був створений за допомогою MCAS).

Отже, як не дивно, вони виявляються дуже корисними :-)


5
"немає жодних замкнених рішень, які безпосередньо використовують лише CAS для будь-якої структури даних O (logn)". Неправда. Для зустрічних прикладів дивіться cl.cam.ac.uk/research/srg/netos/lock-free
Джон Харроп

-1

Перелік пропускних списків має перевагу зняття блокування. Але час виконання часу залежить від того, як визначається рівень нового вузла. Зазвичай це робиться за допомогою Random (). У словнику з 56000 слів пропуск списку зайняв більше часу, ніж у хитрого дерева, а дерево зайняло більше часу, ніж хеш-таблиця. Перші два не змогли відповідати виконанню хеш-таблиці. Також масив хеш-таблиці може бути заблокований одночасно, позбавлений.

Пропускний список та подібні упорядковані списки використовуються, коли потрібна місцепосилання. Наприклад: пошук польотів поруч і перед побаченням у додатку.

Імморіальне дерево бінарного пошуку є чудовим і частіше використовується.

Пропустити список Vs Splay Tree Vs Hash Table Час виконання у словнику знайти op


Я швидко поглянув, і ваші результати, схоже, показують SkipList так само швидше, ніж SplayTree.
Chinasaur

Неправильно вважати рандомізацію частиною пропускного списку. Те, як пропускаються елементи, є вирішальним. Для імовірнісних структур додається рандомізація.
user568109
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.