B-деревья против двоичных деревьев

#performance #binary-tree #b-tree

#Производительность #двоичное дерево #b-tree

Вопрос:

Если я реализую операцию поиска в памяти (RAM) с b-деревьями, то будет ли это лучше с точки зрения кэширования или некоторых других эффектов по сравнению с двоичными деревьями?

Что я знаю, так это-

 binary search tress---O(log n)
btrees ---------------O(c log n)
  

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

Комментарии:

1. Предположительно, любой данный поиск посещает каждый узел только один раз, так какому улучшению способствовал бы кэш? Не зная размера узла, невозможно даже дико рассуждать о размере строки кэша, улучшающем следующий попадание.

Ответ №1:

Алгоритмическая сложность та же, поскольку O (logb n) = O (c log n) = O (log n) но постоянные коэффициенты, которые скрыты в нотации big-O, могут заметно различаться в зависимости от реализации и аппаратного обеспечения.

B-деревья были разработаны для жестких дисков platter, которые имеют большое время доступа (перемещение головки в нужное положение), после чего считывается весь физический сектор. Создание узлов B-дерева размером с сектор сводит к минимуму количество обращений и максимизирует полезные данные при каждой операции чтения.

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

Например, давайте спланируем структуру данных для хранения 220 ключей по 1 слову каждый, в общей сложности 4 МБ необработанных данных на 32-разрядной машине.

Бинарное дерево поиска будет иметь 220 узлов, каждый из которых содержит один ключ и два указателя (3 слова). Глубина будет равна log2(220) = 20. Средний поиск должен будет считывать ключ и один из указателей из каждого узла на своем пути, от корня до конца = 40 слов.

B-дерево, созданное для жестких дисков, будет иметь узлы размером 4 КБ. Каждый узел может храниться внутри в виде отсортированного массива пар ключей и указателей, от 256 до 512 из них. Как будет выглядеть средний поиск? Учитывая среднее заполнение на 3/4, каждый узел будет содержать 384 записи, и его внутренний двоичный поиск должен будет посещать в среднем 2(384) = 5,95 ключа журнала. Средняя глубина будет составлять 384 логарифма (220) = 2,33, поэтому для нашего поиска придется прочитать в среднем 2,33 раза по 5,95 ключа, или около 14 слов.

В случае B-дерева с низким коэффициентом разветвления (коэффициент ветвления), когда каждый узел содержит от 16 до 32 ключей, среднее заполнение составит 24 ключа, средняя глубина log24(220) = 4,36, двоичный поиск в каждом узле произведет сравнения log2 (24) = 4,58, а общий средний поиск должен будет содержать около 20 слов.

Имейте в виду, что последние две структуры данных достигают лучшего результата, чем двоичные деревья, потому что они оптимизируют операции чтения по сравнению с изменениями. Чтобы вставить ключ в одно из этих B-деревьев, вам пришлось бы переписать в среднем весь узел из 384 слов или 24 слов, если не более одного, в то время как в случае с двоичным деревом операции записи все равно потребовалось бы затронуть только до 40 слов.

(Ранее у меня это было неправильно. Спасибо @virco и @Groo за указание на мою ошибку в комментариях.)

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

32 ключа на узел, в частности, кажутся оптимальным вариантом для современных архитектур, как 32-, так и 64-разрядных. Многие новые языки и библиотеки используют 32-ключевые B-деревья в качестве встроенной структуры данных, наряду с хэш-таблицами и массивами или в качестве замены им. Это использование было инициировано Clojure и другими функциональными языками, но впоследствии было подхвачено более распространенными языками, такими как Javascript, с недавним акцентом на неизменяемые структуры данных (например. Immutable.js )

Этот результат может быть объяснен не только подсчетом количества слов, считываемых из памяти, но и пропусками в кэше, которые являются операциями чтения, которые заставляют процессор зависать и ждать ОЗУ. Если архитектура кэширования может извлекать фрагменты оперативной памяти, содержащие целый узел B-дерева за раз, мы получаем ту же оптимизацию, которая успешно использовалась для массового хранилища на диске.

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

Комментарии:

1. Хорошее подробное описание. Действительно ценный. Становится ясно, почему B-деревья в целом лучше, если они хранятся во вторичной памяти

2. Спасибо. Это не просто вторичная память, она работает и с кэшем L3. Я добавил эту часть.

3. Не забывайте, что сегодня размер строки кэша процессора обычно составляет 64 байта, а размер сектора дисков (вращающихся и SSD) часто составляет 512 байт…

4. Ну, обычно реализации B-дерева выполняют двоичный поиск внутри узла, поэтому количество сравнений примерно такое же, как и в BST. Исключения часто делаются для B-деревьев с низким разветвлением (5-10-20) в памяти, где линейное сканирование выполняется быстрее.

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

Ответ №2:

B-деревья отличаются от двоичных деревьев тем, что ключи и указатели кластеризованы в памяти, поэтому вы получаете несколько лучшее поведение кэша как на диске, так и в памяти. Однако в асимптотической (big-O) среде выполнения нет разницы.

Комментарии:

1. Я думаю, что BTree имеет сложность поиска O (log (b) N), где B — класс дерева. Таким образом, в отличие от O (log (2) N), существует большая разница в производительности. Поправьте меня, если я ошибаюсь

2. Исправляя вас, затем: Основание логарифма фактически является постоянным множителем на логарифм (например, log10(n) равно 3.32 * log2(n) ) , поэтому оно игнорируется для классов сложности, как и любой другой постоянный множитель. Поиск в любом типе деревьев по-прежнему выполняется O(log n) .