Головна▸Статті▸Фізіологія

Кислотно-лужна рівновага

pH крові суворо контролюється.

mysimulator teamОновлено — червень 2026≈ 8 хв читання▶ Відкрити симуляцію

Від хеш-кодів до шляхів Trie

HAMT починається звичайної хеш-функції, яка перетворює ключ на ціле число фіксованої ширини, зазвичай тридцять два біти. Замість того, щоб використовувати цей хеш як один індекс масиву, як це робить звичайний хеш-таблиця, HAMT розглядає хеш як послідовність невеликих фрагментів і використовує кожен фрагмент для глибшого переходу в дерево. З розміром фрагменту п'яти біт тридцять два біти дають до семи рівнів фрагментів (останній частковий), і кожен фрагмент може мати 32 значення, оскільки п’ять біт кодують числа від нуля до тридцяти одного. На корені перші п’ять біт хешу вибирають один із тридцяти двох можливих гілок для слідування. На наступному рівні вниз ті ж самі п'ять біт хешу вибирають наступну гілку, і так далі, поки ключ не буде знайдено в листі або дерево не закінчиться рівнями та повернеться до зв’язного списку або вузла зіткнення для ключів, які хешуються однаково. Саме тому розгалуження у 32 є таким важливим: з лише п'ятьма рівнями, необхідними для відрізнення приблизно одного мільярда можливих значень хеш-значення, дерево залишається надзвичайно глибоким навіть для дуже великих колекцій. Порівняно з звичайним бінарним деревом, яке потребує приблизно 30 рівнів для зберігання тієї ж кількості записів. Оскільки глибина зростає як логарифм кількості елементів, але за основою 32 замість основи 2, фактична кількість рівнів, які відвідуються будь-якою реалістичною картою, навіть з мільярдами записів, зазвичай не перевищує шість або сім стрибків. Кожен стрибок виконує невелику константу роботи: витягує наступний фрагмент п’яти біт і використовує його для індексації в масиві. Це пояснює репутацію HAMT щодо майже постійного часу отримання, вставки та видалення операцій, навіть якщо технічно складність логарифмічна, а не дійсно константна. Структура Trie сама по собі не пояснює ефективність пам’яті або історію беззмінності; для цього необхідний механізм бітмапу та підрахунку кількості встановлених бітів, описаний далі.

Бітмап: Позначення наявності дітей

Якщо кожен вузол у цьому дереві пошуку виділяв повний масив з тридцяти двох слотів для зберігання своїх дітей, то більшість цих слотів були б порожніми для будь-якого вузла, який не є щільно заповненим, що є переважною ситуацією в реальних картах. Вузол у нижніх шарах цього дерева міг би мати лише один або два справжніх нащадків з можливих тридцяти двох позицій, тому виділення тридцяти двох покажчиків, більшість з яких були б нульовими, витрачало б величезну кількість пам’яті на мільйонах вузлів. Концепція HAMT вирішує це шляхом використання тридцяти двобітного бітмапу, що зберігається разом із кожним внутрішнім вузлом. Кожна з тридцяти двох бітових позицій у цьому бітмапі відповідає одному з тридцяти двох можливих слотів для дітей на цьому рівні дерева, визначених п’ятибітовим значенням чанку, що використовується для досягнення цього слота. Якщо дитина існує в позиції k, біт на позиції k у бітмапі встановлюється в 1; якщо дитини там немає, біт залишається 0. Отже, замість розрідженого тридцяти двоелементного масиву, вузол зберігає один тридцяти двобічний цілий числовий бітмап плюс компактний масив, розмірений відповідно до кількості фактичних дітей, ні більше і ні менше. Це пряме застосування загальної техніки, яка називається біт-набором або біт-індексованим структурою, і це означає, що використання пам’яті залежить від кількості дійсно збережених елементів, а не від теоретичного розгалуження. Вузол із трьома дітьми використовує бітмап плюс масив з трьох елементів; вузол із тридцятью дітьми використовує той самий розмір бітмапу плюс масив з тридцяти елементів. Сам бітмап займає невелику постійну кількість простору, зазвичай один машинний слово, незалежно від кількості дітей, присутніх. Коли потрібен пошук або оновлення для перевірки, чи існує дитина на заданому п’ятибітному індексі, він просто перевіряє, чи встановлено цей біт у бітмапі – надзвичайно швидка бітова операція. Більш складне питання, яке розглядається далі, полягає в тому, як алгоритм знаходить, де саме в цьому компактному масиві живе ця дитина, оскільки компактний масив не має тридцяти двох слотів для безпосереднього індексування.

Підрахунок нулів: відображення положень бітмапу в індекси масивів

Знання того, що біт встановлено на позиції k у бітмапі, повідомляє вам, що існує дитина, але компактний масив щільно упакований без проміжків, тому дитина не обов’язково знаходиться на індексі k в цьому масиві. Замість цього її положення в компактному масиві дорівнює кількості встановлених бітів у бітмапі нижче k. Ця кількість встановлених бітів відома як підрахунок нулів або підрахунок нулів бітмапу, обмеженого лише бітами перед позицією k. Щоб знайти дитину на бітовій позиції k, алгоритм створює маску, що охоплює біти від нуля до k мінус 1, застосовує бітовий І з цією маскою та бітмапом вузла і підраховує кількість залишків одиниць; це число є точно індексом у компактному масиві, де живе дитина, оскільки кожен встановлений біт нижче позиції k відповідає дитині, яка була упакована в масив до цього одного. Сучасні процесори надають присвячений інструкції підрахунку нулів, часто званий POPCNT, яка обчислює цю кількість за один швидкий крок, тому ця техніка достатньо ефективна для структури даних, призначеної для широкого використання в програмах. Без підтримки апаратного забезпечення підрахунок нулів все ще можна швидко обчислити за допомогою кількох бітових трюків, які обробляють кілька бітів паралельно, а не перебирають кожен з 32 бітів окремо. Ця комбінація бітмапу плюс підрахунок нулів іноді називається стислим або біт-індексованим масивом і зустрічається в інших ефективних за простором структурах даних поза HAMT, таких як лаконічні структури ранжування та вибору, які використовуються в деяких пошукових індексах. Загальний ефект для HAMT полягає в тому, що кожен внутрішній вузол платить лише за дітей, які він насправді має: одне слово бітмапу фіксованого навантаження плюс один слот масиву за кожну реальну дитину, а підрахунок нулів забезпечує міст між розрідженим концептуальним 32-спосібним розгалуженням і щільною фізичною схемою зберігання, все обчислюється на вимогу, а не зберігається явно.

Спільність структури та постійні оновлення

Друга вісь HAMT-дизайну, поряд з бітмапом і прийомом «попкінт», полягає в тому, як він підтримує незмінність без додаткових витрат на повне копіювання при кожному зміні. У послідовному (persistent) структурі даних операція оновлення, така як вставлення або видалення ключа, ніколи не змінює оригінальну структуру на місці; замість цього вона генерує нову версію структури, зберігаючи стару версію повністю незмінною та придатною для використання, що є необхідним для безпечного одночасного доступу та таких функцій, як історія відкатів або знімки. Наївний спосіб досягти цього був би копіювати весь три (trie) при кожному оновленні, але це знищило б переваги від неглибокого дерева. HAMT уникнув цього завдяки структурному обміну: оновлення йде вниз від кореня вздовж тієї ж п’ятибітної групи шляхів, що використовуються для пошуку, і на кожному рівні виділяється новий вузол, який є поверхневою копією старого вузла з одним дочірнім покажчиком доданим, видаленим або заміненим, а також оновлений бітмап, якщо дочірній вузол був доданий або видалено. Ключовим є те, що всі інші дочірні покажчики в новому вузлі все ще вказують на ті самі піддерева, як і в старому вузлі, тобто ці піддерева не копіюються взагалі. Оскільки три має неглибоку структуру, зазвичай від п’яти до семи вузлів глибоко, оновлення виділяє лише стільки нових вузлів, приблизно логарифм за основою 32 з урахуванням розміру колекції, тоді як будь-яка гілка, яка не знаходиться на шляху від кореня до зміненого ключа, залишається спільною між старою та новою версіями карти. Саме тому раніше заявлені логістичні операції часу застосовуються і до вставлення, і до видалення, а не лише до пошуку: робота пропорційна глибині три, а не загальній кількості елементів. Стара версія карти залишається повністю дійсним, незмінним даними, які інші частини програми можуть безпечно використовувати навіть після оновлення, що є гарантією, на яку покладаються функціональні мови для безпечного одночасного доступу без блоків.

Чому це важливо в практичному сенсі

Поєднання глибокого широкого триєра, компактних вузлів з бітовим індексацією та механізм обміну структурами робить HAMTs практичними як стандартне представлення карти та множини у функціональних мовах програмування, а не просто теоретичною цікавістю. Clojure's persistent hash map, яку представив Річ Хайкі та безпосередньо надихнувся ранніми академічними роботами про HAMTs Філом Беґвелем, широко використовується в типовому Clojure коді, оскільки програми можуть передавати значення, що виглядає як звичайна незмінна карта, тоді як внутрішньо зберігаються агресивні обмін даних пам'яті, уникаючи витрат, які б наклали звичайні колекції copy-on-write. Scala's immutable HashMap та HashSet у її стандартній бібліотеці використовують той самий підхід, і подібні техніки бітової індексації триєрів також зустрічаються в реалізації карти в Erlang та Elixir. Практичні переваги проявляються кількома способами. По-перше, накладні витрати пам'яті на кожне оновлення невеликі та передбачувані, пропорційні глибині триєра, а не розміру колекції, тому навіть карти з мільйонами записів можуть дешево оновлюватися тисячами разів на секунду. По-друге, оскільки попередні версії залишаються дійсними та незмінними, кілька потоків можуть одночасно читати спільну карту без жодного блокування, оскільки жоден потік ніколи не змінює дані, які може читати інший потік; нова версія просто стає доступною як окреме значення. По-третє, невелика глибина триєра з широким розгалуженим фактором підтримує продуктивність читання близько до того ж, що й у змінному хеш-таблиці, тому функціональні програми не платять високу продуктивну плату за незмінність, як це було б, наприклад, з невмілою незмінною зв’язковою лінією або незбалансованим деревом. Існують компроміси: HAMTs зазвичай мають деякі постійні коефіцієнти накладних витрат порівняно з простим змінним хеш-таблицем через додаткову індиректність вузлів дерева та обчислення popcount при кожному доступі, і зіткнення хешів все ще потребують механізму резервного копіювання, наприклад, вузли зіткнень, які містять кілька записів. Проте, для мов та систем, яким потрібні справді незмінні колекції у великому масштабі, HAMT залишається стандартною, добре зрозумілою відповіддю.

Frequently asked questions

Чому використовувати п’ять біт на рівень замість іншого розміру чанку?

П'ять біт забезпечують розгалуження у вісімдесят два, що є широко поширеним «світлим місцем» між глибиною дерева та шириною вузла. Менший чанк, наприклад, чотири біти, дає розгалуження в шістнадцять шляхів і глибше дерево з більшою кількістю рівнів для перебору на кожній операції. Більший чанк, наприклад, шість біт, забезпечує розгалуження у шістдесят чотири шляхи та більш похилі дерева, але кожен вузол битової мапи потребував би шістдесят чотири біт замість тридцяти двох, що подвоює фіксований накладний витрат і лише незначно зменшує глибину. Розгалуження у вісімдесят два шляхи дозволяють бітовій мапі точно поміститися в одне машинне слово на більшості процесорів, що робить бітові операції, включаючи popcount, швидкими та простими, тому п’ять біт на рівень стало звичайним вибором у більшості виробничих реалізацій HAMT.

Що відбувається, коли два різні ключі генерують однаковий хеш-код?

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

Чи є HAMT однаковою річчю, як звичайний хеш-таблиця?

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

Чи потребує масштабування HAMT перехешування всього, як у типовій хеш-таблиці?

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

Як popcount дійсно обчислюється швидко на тридцяти двох бітній бітмапі?

На більшості сучасних процесорів процесор пропонує батьківську інструкцію, часто названу POPCNT, яка безпосередньо підраховує кількість одиничних бітів у машинному слові апаратним способом, зазвичай завершуючи це в один цикл процесора або близько до нього. Коли така інструкція недоступна, програмні реалізації використовують добре відому послідовність бітових операцій, які повторно об'єднують сусідні групи бітів, наприклад спочатку підраховуючи біти в парах, потім у групах по чотири, потім вісім і так далі, завершуючи це приблизно п’ятьма або сімома простими операціями замість перебору всіх тридцяти двох біт окремо. У будь-якому випадку обчислення кількості населення, необхідної для пошуку індексу компактного масиву дитини, є надзвичайно швидким відносно вартості доступу пам'яті, яка не стає вузьким місцем.

Спробуйте наживо

Усе, що вище, працює прямо у вашому браузері — відкрийте HAMT: Hash Array Mapped Trie і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.

▶ Відкрити симуляцію HAMT: Hash Array Mapped Trie

Що ви знайшли?

Додати кроки відтворення (опційно)