Вы находитесь на странице: 1из 43

Перевод: английский - русский - www.onlinedoctranslator.

com

Руби под
микроскоп
Изучение внутреннего устройства Ruby с помощью

эксперимента Пэт Шонесси


Превью одной главы — май 2012 г.

Обсуждение/отзывы:
http://patshaughnessy.net/ruby-under-a-microscope
@pat_shaughnessy
Оглавление

Введение ................................................. ................................................. ................................................ 1


Зачем изучать внутренности Ruby? ................................................. ................................................. 1
Мой подход в этой книге: теория и эксперимент ................................................. .............................. 2
Как масштабируются хэши от одного до миллиона элементов ................................................. ................................ 3

Теория: хеш-таблицы в Ruby ................................................. ................................................. .............. 4


Эксперимент 1: получение значения из хэшей разного размера ................................................. ........... 9

Теория: как хеш-таблицы расширяются, чтобы вместить больше значений ................................................. ...... 10

Эксперимент 2: вставка одного нового элемента в хэши разного размера .............................................. 14

Теория: почему хэши будут быстрее в Ruby 2.0 ................................................. ................................ 18


Эксперимент 3: вставка одного нового элемента в хэши разного размера для Ruby 2.0 ......................... 20

Теория: как Ruby реализует хеш-функции ................................................. ................................ 22


Эксперимент 4: Использование объектов в качестве ключей в хеше ................................................. ................................ 26

Теория: как Ruby сохраняет информацию о заказе в хэшах ................................................. ...................... 30

Эксперимент 5: перебор элементов, вставленных в хэш ................................................. ............... 32


Альтернативные теории: Хэши в JRuby ................................................. ................................................. 33
Альтернативные теории: Хэши в Рубиниусе ................................................. ............................................. 36
Вывод ................................................. ................................................. ................................................ 38
Приложение: Код эксперимента ................................................. ................................................. ............ 39
Рубин под микроскопом

Введение

Зачем изучать внутренности Ruby?

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

часто вы когда-нибудь задумывались о том, как на самом деле работает ваш автомобиль? Когда в прошлые выходные вы

останавливались на красный свет по пути в продуктовый магазин, думали ли вы о теории и технике, лежащих в основе

двигателя внутреннего сгорания? Нет, конечно нет! Все, что вам нужно знать о своем автомобиле, — это какая педаль, как

поворачивать руль и несколько других важных деталей, таких как переключение передач, индикаторы поворота и т. д.

На первый взгляд, изучение внутренней реализации Ruby ничем не отличается: зачем вообще изучать, как был

реализован язык, когда все, что вам нужно сделать, это использовать его? Кого волнует, например, как объекты

Ruby Array или Hash работают внутри; все, что мне нужно знать, это базовое использование — как добавить что-

то в массив, как вернуть его позже.

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

внутренней реализации Ruby:

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

как Мац и остальная часть основной команды Ruby планировали использовать язык. Вы узнаете,

что работает хорошо, что работает быстро, а что нет. Вы станете лучшим разработчиком Ruby,

если будете использовать язык так, как он был задуман, а не только так, как вы предпочитаете.

• Вы можете многое узнать о компьютерных науках. Помимо того, что вы цените талант и дальновидность

основной команды Ruby, вы сможете учиться на их работе. При реализации языка Ruby основная команда

должна была решить многие из тех же проблем информатики, которые вам, возможно, придется решать в

вашей работе или в проекте с открытым исходным кодом. Очевидно, что это не относится к аналогии с

вождением автомобиля; изучение механических и электрических деталей двигателя вашего автомобиля не

поможет вам стать лучшим водителем.

• Это весело! Я нахожу изучение алгоритмов и структур данных, которые использует Ruby, увлекательным, и я надеюсь, что

вы тоже это сделаете.

1
Рубин под микроскопом

Мой подход в этой книге: теория и эксперимент

«Неважно, насколько красива твоя теория, неважно, насколько ты умен. Если это не
согласуется с экспериментом, это неправильно». - Ричард Фейнман

ВРубин под микроскопом Я собираюсь научить вас, как Ruby работает внутри. Я буду использовать ряд простых и понятных

диаграмм, которые покажут вам, что происходит внутри, когда вы запускаете программу на Ruby. Подобно физику или химику, я

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

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

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

или иную функцию Ruby.

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

объяснения некоторых аспектов внутреннего устройства Ruby, некоторых функций или поведения языка я проведуэксперимент

чтобы доказать, что моя теория верна. Для этого я буду использовать Ruby, чтобы протестировать себя! Я запущу несколько

небольших тестовых сценариев Ruby и посмотрю, дают ли они ожидаемый результат, работают ли они так же быстро или

медленно, как я ожидаю, действительно ли Ruby ведет себя так, как говорит моя теория.

Я предполагаю, что вы разработчик Ruby, который использует язык каждый день и заинтересован в том, чтобы узнать больше о

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

основная группа разработчиков Ruby использовали для создания интерпретатора. Я не буду шаг за шагом проходить код C,

объясняя все входы и выходы деталей кодирования C. Если вы заинтересованы в подобных вещах, то вашим лучшим ресурсом

будетРуководство по взлому Ruby , первоначально написанный на японском языке и частично переведенный на английский язык.

Тем не менее, для тех, кто знаком с C, я покажу несколько очень упрощенных фрагментов кода C,

чтобы дать вам более конкретное представление о том, что происходит внутри Ruby. Я также

укажу, в каком исходном коде MRI C я нашел фрагмент; это облегчит вам самостоятельное

изучение кода MRI C, если вы когда-нибудь решите это сделать. Как и в этом абзаце, я буду

отображать эту информацию на желтом фоне.

Если вас не интересуют подробности кода C, просто пропустите эти желтые разделы.

2
Рубин под микроскопом

Как масштабируются хэши от одного до миллиона элементов

Вы, вероятно, очень хорошо знаете, как использовать объект Hash в своих программах на Ruby, но знаете ли вы, в чем

заключается самая замечательная и важная особенность объекта Ruby Hash? Что делает объект Hash интересным, так это не то,

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

в нем элементов.

Вот некоторые данные, подтверждающие это:

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

размеров. Ось Y показывает, сколько времени в миллисекундах потребовалось моему тестовому коду Ruby для извлечения 10 000

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

ключей, которые Ruby пришлось искать, чтобы найти ключ, который я запросил. Ясно, что объект Ruby Hash работает очень

быстро; на моем ноутбуке он может искать и находить ключ в хеше 10 000 раз примерно за 1,5 мс. Если посчитать, в среднем Ruby

требуется всего 0,15микросекундычтобы найти заданный ключ и вернуть значение.

3
Рубин под микроскопом

Но что действительно удивительно, так это не только то, что Ruby быстр, но и то, что Ruby одинаково быстр для хеша,

содержащего миллион ключей, как и для хеша, содержащего только один ключ! Что примечательно в этой диаграмме, так это то,

что она более или менее плоская.

Если вы задумаетесь об этом на минуту, объект Hash на самом деле представляет собой мини-поисковик: каким-то образом Ruby

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

только одно значение, которое соответствует к этому ключу. Точно так же, когда вы сохраняете новую пару ключ/значение в

хэш, Ruby сначала быстро определяет, присутствует ли уже значение для этого ключа, и перезаписывает его, если оно есть. Как

оно работает? Создает ли Ruby какой-либо поисковый индекс?

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

разные ячейки, что позволит Ruby впоследствии очень и очень быстро искать их. Я также объясню, как хеш-таблицы

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

получить некоторые данные, доказательства того, что Ruby действительно использует хеш-таблицы и хэш-функции для

внутренней реализации объекта Hash. Наконец, я исследую, как хэши сохраняют информацию о порядке в хеш-таблице —

возвращаются ли они в том же порядке, в котором я их вставил?

Теория: хеш-таблицы в Ruby

Изучив код MRI C, который Ruby использует для реализации объекта Hash, я очень быстро

обнаружил, что Ruby использует так называемую «хеш-таблицу» для сохранения ключей и

значений, которые вы сохраняете в любом хеше. Хэш-таблицы — это

широко используемая, хорошо известная, старая концепция в информатике. Они организуют значения в группы или

«ячейки» на основе целочисленного значения, вычисляемого из каждого значения, называемого «хэшем». Позже,

когда вам нужно найти и найти значение, пересчитав хэш-значение, вы сможете выяснить, в какой ячейке

содержится значение, что ускорит поиск.

Вот высокоуровневая диаграмма, показывающая один хэш-объект и его хеш-таблицу:

4
Рубин под микроскопом

Слева структура «RHash»; это сокращение от «Ruby Hash». Все другие важные типы объектов, используемые внутри

Ruby, представлены похожими структурами, называемыми «RFile», «RArray», «RValue» и т. д. Каждая из этих структур,

включая RHash, содержит набор внутренних системных значений об этом объекте. которые Руби должен

отслеживать.

Справа я показываю хэш-таблицу, используемую этим хешем, представленную структурой «st_table». Эта структура C

содержит основную информацию о хеш-таблице, такую как количество записей, сохраненных в таблице, количество

ячеек и указатель на ячейки. Каждая структура RHash содержит указатель на соответствующую структуру st_table. Наконец,

я показываю несколько пустых контейнеров в правом нижнем углу. Ruby 1.8 и Ruby 1.9 изначально создают 11 бинов для

нового пустого хэша.

Лучший способ понять, как работает хэш-таблица, — это рассмотреть пример. Предположим, я добавляю

новый ключ/значение в хэш с именем «my_hash:».

мой_хэш[:ключ] ="ценность"

При выполнении этой строки кода Ruby создаст новую структуру под названием «st_table_entry» и

сохранит ее в хеш-таблице для «my_hash:».

5
Рубин под микроскопом

Здесь вы можете видеть, что Ruby сохранил новую пару ключ/значение в третьей корзине, #2. Ruby сделал это, взяв

заданный ключ (символ «:key» в этом примере) и передав его внутренней хеш-функции, которая возвращает

псевдослучайное целое число:

некоторое_значение = внутренняя_хэш-функция (: ключ)

Затем Ruby берет значение хеш-функции, в данном примере «some_value», и вычисляет модуль по

количеству бинов… то есть остаток после деления на количество бинов:

некоторое_значение%11знак равно2

На этой диаграмме я представляю, что фактическое значение хеш-функции для «:key», разделенное на 11, дает в остатке 2.

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

Теперь добавим в хеш второй элемент:

мой_хэш[:key2] ="значение2"

И на этот раз давайте представим, что хеш-значение «:key2», деленное на 11, дает в остатке 5:

6
Рубин под микроскопом

внутренняя_хэш_функция (: key2) %11знак равно5

Теперь вы можете видеть, как Ruby помещает вторую структуру «st_table_entry» в ячейку №5, шестую ячейку:

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

помещает my_hash[:key]

=>"ценность"

Если бы Ruby сохранил все ключи и значения в массиве или связанном списке, ему пришлось бы перебирать все

элементы в этом массиве списка в поисках :key. Это может занять очень много времени, в зависимости от

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

пересчитав хеш-значение для этого ключа. Он просто снова вызывает хеш-функцию:

7
Рубин под микроскопом

некоторое_значение = внутренняя_хэш-функция (: ключ)

… переделывает хеш-значение на количество бинов и получает остаток, модуль:

некоторое_значение%11знак равно2

… и теперь Ruby знает, что нужно искать в корзине № 2 запись с ключом :key. Аналогичным образом Ruby может позже

найти значение для :key2, повторив тот же расчет хеш-функции:

внутренняя_хэш_функция (: key2) %11знак равно5

Хотите верьте, хотите нет, но библиотека C, используемая Ruby для реализации хеш-таблиц, была

первоначально написана еще в 1980-х годах Питером Муром из Калифорнийского университета в

Беркли, а затем модифицирована основной командой Ruby. Вы можете найти код хеш-таблицы Питера

Мура в файлах кода C «st.c» и «include/ruby/st.h». Все имена функций и структур используют соглашение

об именах «st_» в коде хеш-таблицы Питера.

Код хэш-таблицы Питера Мура играет очень важную и центральную роль во внутреннем устройстве Ruby. Он

используется не только объектом Hash, но и во многих других местах, например, для отслеживания того, какие

методы определены в каждом объектном классе или модуле Ruby. Другими словами, Ruby использует код хэш-

таблицы Питера Мура для отслеживания своих внутренних данных, а не только ваших данных, которые вы

сохраняете в объектах Hash.

Между тем, определение структуры «RHash», которая представляет каждый объект Ruby Hash,

можно найти в файле include/ruby/ruby.h. Наряду с RHash здесь вы найдете все другие основные

структуры объектов, используемые в исходном коде Ruby: RString, RArray, RValue и т. д.

8
Рубин под микроскопом

Эксперимент 1: получение значения из хэшей


разного размера

Мой первый эксперимент будет создавать хэши самых разных размеров, от 1 элемента до

1 миллиона элементов, а затем измерять, сколько времени потребуется, чтобы найти и

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

тестового кода в приложении илина Гитхабе если вы хотите попробовать это

самим собой. А пока вот важные фрагменты тестового кода. Во-первых, я создаю хэши разных размеров на

основе степеней двойки, запуская этот код для разных значений «экспоненты»:

размер =2**экспонента

хэш = {}

(1..размер).каждый делать |н|

индекс = ранд

хэш[индекс] = ранд

конец

Здесь и ключи, и значения являются случайными плавающими значениями. Затем я измеряю, сколько времени требуется, чтобы

найти один из ключей, «target_key», 10 000 раз, используя тестовую библиотеку:

Benchmark.bm сделать |bench|

скамейка.отчет("извлечение элемента из хеша с элементами #{size} 10000 раз") делать

10000.раз делать

val = хэш[target_key]

конец

конец

конец

9
Рубин под микроскопом

Результаты: маленькие или очень большие хэши Ruby работают одинаково быстро!

Я уже показывал этот график выше; результаты поразительны: используя внутреннюю хеш-таблицу, Ruby может находить и

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

небольшого хеша:

Очевидно, что хеш-функция, которую использует Ruby, очень быстра, и как только Ruby идентифицирует бин,

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

примечательно то, что значения на этой диаграмме более или менее плоские.

Теория: как хеш-таблицы расширяются,


чтобы вместить больше значений

В этот момент вы можете подумать о будущем и спросить себя: если

существуют миллионы структур st_table_entry, почему распределение их по

11 корзинам помогает Ruby быстро выполнять поиск? Даже если хэш-

функция быстрая, и даже если Ruby равномерно распределяет значения

10
Рубин под микроскопом

среди 11 бинов в хеш-таблице Ruby все равно придется искать среди почти 100 000 элементов в каждом бине,

чтобы найти целевой ключ, если всего есть миллион элементов.

Здесь должно быть что-то еще. Мне кажется, что Ruby должен добавлять в хеш-таблицу больше бинов по мере

добавления все новых и новых элементов. Давайте еще раз посмотрим, как работает код внутренней хеш-таблицы Ruby.

Продолжая пример выше… предположим, я продолжаю добавлять все больше и больше элементов в свой хэш:

мой_хэш[:key3] = "значение3"

мой_хэш[:key4] = "значение4"

мой_хэш[:key5] = "значение5"

мой_хэш[:key6] = "значение6"

. . . так далее ...

По мере того, как я добавляю все больше и больше элементов, Ruby будет продолжать создавать больше структур st_table_entry

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

Ruby использует связанный список для отслеживания записей в каждой ячейке: каждая структура st_table_entry содержит указатель на

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

и длиннее.

Чтобы эти связанные списки не вышли из-под контроля, Ruby измеряет так называемую «плотность» или среднее

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

11
Рубин под микроскопом

количество записей в ячейке увеличилось примерно до 4. Это означает, что модуль хеш-значений 11 начал возвращать

повторяющиеся значения для разных ключей и хэш-значений. Таким образом, при поиске целевого ключа Ruby может

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

содержит нужную запись.

Как только плотность превысит 5, постоянное значение в исходном коде MRI C, Ruby выделит больше бинов, а затем

«перехэширует» или перераспределит существующие записи среди нового набора бинов. Например, если я продолжу добавлять

больше пар ключ/значение, через некоторое время Ruby отбросит массив из 11 ячеек, выделит массив из 19 ячеек, а затем

перефразирует все существующие записи:

Теперь на этой диаграмме плотность бинов упала примерно до 3.

Отслеживая таким образом плотность бинов, Ruby может гарантировать, что связанные списки останутся короткими, а

извлечение хеш-элемента всегда будет быстрым — теперь, после вычисления хеш-значения, Ruby просто нужно пройти через 1

или 2 элемента, чтобы найти нужный элемент. целевой ключ.

Вы можете найти функцию «rehash» — код, который перебирает структуры st_table_entry и

пересчитывает, в какую ячейку поместить запись — в исходном файле st.c примерно на

строке 316 в Ruby 1.8.7:

статическая пустота

перефразировать (таблица)

зарегистрируйте st_table *table;

зарегистрируйте st_table_entry *ptr, *next, **new_bins;

int i, old_num_bins = table->num_bins, new_num_bins;

12
Рубин под микроскопом

беззнаковое целое hash_val;

new_num_bins = новый_размер (old_num_bins+1);

new_bins = (st_table_entry**) Calloc (new_num_bins, sizeof (st_table_entry*));

для (я =0; я < old_num_bins; я++) {

ptr = table->bins[i];

в то время как (ptr !=0) {

следующий = указатель-> следующий;

hash_val = ptr->hash % new_num_bins;

ptr->next = new_bins[hash_val];

new_bins[hash_val] = указатель;

птр = следующий;

бесплатно (таблица-> корзины);

таблица->num_bins = новое_num_bins;

таблица->бины = новые_бины;

Здесь вызов метода «new_size» возвращает новое количество бинов, например 19. Как только Ruby получает

новое количество бинов, он выделяет новые бины, а затем выполняет итерацию по всем существующим

структурам st_table_entry — всем парам ключ/значение в хэше. Для каждой записи st_table_entry Ruby

пересчитывает позицию ячейки, используя ту же формулу модуля: hash_val = ptr->hash % new_num_bins. Затем

он сохраняет каждую запись в связанном списке для этой новой корзины. Наконец, Ruby обновляет структуру

st_table и освобождает старые корзины.

В Ruby 1.9 и Ruby 2.0 функция перефразирования реализована несколько иначе, но

работает по существу одинаково.

13
Рубин под микроскопом

Эксперимент 2: вставка одного нового элемента в хэши


разного размера

Один из способов проверить, действительно ли происходит это повторное хэширование или

перераспределение записей, — измерить количество времени, которое Ruby требуется для сохранения

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

и больше элементов в один и тот же хеш, в какой-то момент я должен увидеть доказательства того, что

Руби тратит дополнительное время на перефразировку элементов.

Я сделаю это, создав 10 000 хэшей одинакового размера, обозначенного переменной «размер»:

хэши = []

10000.раз делать

хэш = {}

(1..размер).каждый делать |х|

хэш[ранд] = ранд

конец

хэш << хэш

конец

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

- размер элемента +1:

Benchmark.bm сделать |bench|

скамейка.отчет("добавление номера элемента #{size+1}") делать

10000.раз сделать |n|

hashes[n][size] = ранд

конец

конец

конец

Итог: вставка 67-го элемента занимает гораздо больше времени!

То, что я обнаружил, было удивительно! Вот данные для Ruby 1.8:

14
Рубин под микроскопом

Интерпретация этих значений данных слева направо:

• Вставка первого элемента в пустой хэш занимает около 9 мс (10000 раз).


• Затем требуется около 7 мс, чтобы вставить второй элемент в хеш, содержащий одно значение (10000

раз).

• Затем, по мере увеличения размера хеша с 2, 3 примерно до 60 или 65, время, необходимое для вставки нового

элемента, постепенно увеличивается.

• Наконец, мы видим, что вставка каждой новой пары ключ/значение в хэш, содержащий 64, 65 или 66 элементов

(10000 раз), занимает около 11 или 12 мс.

• Затем мы видим огромный всплеск! Вставка 67-й пары ключ/значение занимает в два раза больше времени:

около 26 мс вместо 11 мс для 10000 хэшей!

• Наконец, после вставки 67-го элемента время, необходимое для вставки дополнительных элементов, падает

примерно до 10 мс или 11 мс, а затем снова медленно увеличивается.

15
Рубин под микроскопом

Что тут происходит? Что ж, дополнительное время, необходимое для вставки этой 67-й пары ключ/значение, тратится на то, что

Ruby перераспределяет массив бинов с 11 бинов на 19 бинов, а затем переназначает структуры st_table_entry новому массиву

бинов.

Вот тот же график для Ruby 1.9 — вы можете видеть, что на этот раз порог плотности бинов другой. Вместо того, чтобы тратить

дополнительное время на перераспределение элементов по ячейкам при 67-й вставке, Ruby 1.9 делает это при вставке 57-го

элемента. Позже вы можете увидеть, что Ruby 1.9 выполняет еще одно перераспределение после вставки 97-го элемента.

Если вам интересно, откуда берутся эти магические числа, 57, 97 и т. д., взгляните на

верхнюю часть файла кода «st.c» для вашей версии Ruby. Вы должны найти такой список

простых чисел:

16
Рубин под микроскопом

/*

Таблица простых чисел 2^n+a, 2<=n<=30.

*/

static const беззнаковое целое число простых чисел [] = {

8+ 3,

16 + 3,

32 + 5,

64 + 3,

128 + 3,
256 + 27,

512 + 9,

. . . так далее...

В этом массиве C перечислены некоторые простые числа, которые встречаются вблизи степеней двойки. Код

хеш-таблицы Питера Мура использует эту таблицу, чтобы решить, сколько ячеек использовать в хеш-таблице.

Например, первое простое число в приведенном выше списке — 11, поэтому хеш-таблицы Ruby начинаются с 11

бинов. Позже, по мере увеличения количества элементов, количество бинов увеличивается до 19, а еще позже

до 37 и т. д.

Ruby всегда устанавливает количество ячеек хэш-таблицы как простое число, чтобы повысить вероятность того,

что хэш-значения будут равномерно распределены между ячейками, после вычисления модуля - после деления

на простое число и использования остатка. Математически здесь помогают простые числа, поскольку они с

меньшей вероятностью имеют общий множитель с целыми числами хэш-значения, на случай, если плохая хеш-

функция часто возвращает значения, которые не были полностью случайными. Если хеш-значения и количество

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

быть одним и тем же… что приводит к неравномерному распределению записей таблицы между ячейками.

В другом месте файла st.c вы сможете найти эту константу C:

# определить ST_DEFAULT_MAX_DENSITY5

17
Рубин под микроскопом

… который определяет максимально допустимую плотность или среднее количество элементов в ячейке.

Наконец, вы также должны быть в состоянии найти код, который решает, когда выполнять

перераспределение бункера, выполнив поиск, где эта константа ST_DEFAULT_MAX_DENSITY используется

в st.c. Для Ruby 1.8 вы найдете этот код:

if (table->num_entries/(table->num_bins) > ST_DEFAULT_MAX_DENSITY) {

перефразировать(таблица);

Таким образом, Ruby 1.8 пересчитывает от 11 до 19 бинов, когда num_entries/11 больше 5… т. е.

когда оно равно 66… когда вы вставляете 67-й элемент.

Вместо этого для Ruby 1.9 и Ruby 2.0 вы найдете этот код:

if ((таблица) -> num_entries > ST_DEFAULT_MAX_DENSITY * (table) -> num_bins) {

перефразировать(таблица);

Вы можете увидеть рехеширование Ruby 1.9 в первый раз, когда num_entries больше 5*11 или когда

вы вставляете 57-й элемент.

Теория: почему хэши будут быстрее в Ruby 2.0

Выше я показывал графики для Ruby 1.8 и Ruby 1.9 — а как же Ruby 2.0? Он работает как-то

иначе? Перераспределяет ли он бины таким же образом? При чтении исходного кода Ruby

2.0 я заметил множество изменений в коде, связанных с выделением бинов, что заставило

меня заподозрить, что хэши будут работать еще быстрее в

Руби 2.0. Давайте взглянем….

Хэши в Ruby 2.0 будут использовать несколько иные структуры данных для сохранения ключей и значений в хеш-таблице.

Вместо того, чтобы выделять бины и затем назначать записи ключ/значение (структуры st_table_entry) в бины, Ruby 2.0

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

выглядит:

18
Рубин под микроскопом

В исходном коде MRI эти записи называются «упакованными». Здесь вы можете видеть, что ключи и значения сохраняются прямо

внутри корзин в правом нижнем углу, и что в таблице st_table есть новое значение, называемое «entries_packed». Это значение

устанавливается равным true всякий раз, когда ключи и значения сохраняются таким образом. Когда это значение равно false, это

означает, что элементы данных сохраняются в структурах st_table_entry, как обычно.

Но подождите минутку! Если ведер нет, то это вообще не хеш-таблица, не так ли? Правильно: в Ruby 2.0
объект Hash фактически реализован внутри как массив, а не как хеш-таблица!

Однако это верно только для небольших хэшей — хэшей, все элементы которых помещаются в пространство памяти, обычно

используемое для массива бинов. Как только элементы хэша больше не помещаются в память массива бинов, данные ключа/

значения будут скопированы обратно в новые структуры st_table_entry и назначены бинам хеш-таблицы, как обычно.

Оптимизация здесь на самом деле не столько в экономии памяти, сколько во времени/скорости. В Ruby 2.0 основная

команда Ruby решила, что для небольших хэшей на самом деле быстрее просто сохранять ключи и значения в массиве и

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

целевого ключа, который должен быть быстрым, но может быть медленным, если ключ представляет собой большую

строку или массив. Ruby также экономит время, избегая необходимости создавать и настраивать структуры st_table_entry

при вставке элементов.

19
Рубин под микроскопом

Эксперимент 3: вставка одного нового элемента в хэши


разного размера для Ruby 2.0

Для этого теста я повторно запущу тот же код, что и в эксперименте №2, но на этот раз с

использованием сборки «ruby-head»; код из основной ветки, предназначенный для предстоящего

выпуска Ruby 2.0. И я сосредоточусь на данных для меньших размеров хэшей, чтобы посмотреть,

смогу ли я найти какие-либо доказательства этой оптимизации.

Результаты: вставка первых шести элементов выполняется быстрее!

Вот результаты:

Опять же, здесь ось Y показывает время, необходимое для вставки одного нового элемента в

существующий хэш. По оси X я измеряю это время для хэшей разного размера, начиная с нуля (пустой

хеш). Вот как я интерпретирую результаты:

Вот как я интерпретирую результаты:

20
Рубин под микроскопом

• Для вставки первого элемента требуется около 6 мс, 10 000 раз.

• Затем требуется всего около 3 мс, чтобы вставить второй элемент, 10 000 раз.

• После этого Ruby 2.0 требуется от 4 до 5 мс для вставки элементов с номерами 3, 4, 5 и 6.


• Затем мы видим всплеск: для вставки седьмого элемента (10 000 раз) Ruby требуется около 12 мс.

• Наконец, после этого вставка новых элементов занимает в среднем от 6 до 7 мс.

Что это значит?

• Вставка первого элемента занимает немного больше времени, но в Ruby 2.0 массив bin вообще не создается для

пустых хэшей. Вы можете рассматривать это как еще одну оптимизацию: пустые хэши имеют

Структура RHash и структура st_table, но вообще нет массива бинов. Это позволяет Ruby быстрее создавать новые

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

• Тогда вставка элементов до #6 выполняется очень быстро, так как они сохраняются непосредственно в массиве

bin, а Ruby 2.0 вообще не нужно выделять новые структуры st_table_entry или вызывать хеш-функцию.

• Но в массив помещаются только 6 элементов. Поэтому при вставке седьмого элемента Ruby 2.0 должен

создать 7 структур st_table_entry, вызвать хеш-функцию для каждого ключа и назначить их бинам.

Другими словами, Ruby 2.0 должен настроить хеш-таблицу в первый раз, когда вы вставляете седьмой

элемент!

• После этого все работает так же, как и в Ruby 1.9.

Если вам интересно узнать больше о том, как работает новая оптимизация хэш-массива в Ruby 2.0,

найдите слово «упакованный» в файле st.c. Например, в Ruby 2.0 структура st_table содержит

значение с именем «entries_packed», которое указывает, действительно ли хэш является массивом.

Существует также функция «add_packed_direct», которая вставляет новый ключ/значение в хеш-

массив (а не в хеш-таблицу). Наконец, если вы ищете константу с именем MAX_PACKED_HASH, вы

найдете вычисление, которое определяет, что 6 элементов хэша должны быть сохранены в виде

массива, а 7 или более элементов хэша должны быть сохранены в хеш-таблице.

И последнее замечание: это может показаться ненужной и неважной оптимизацией. В конце концов, когда у вас есть 7

элементов в хеше, оптимизация больше не применяется. Но подумайте о том, как часто разработчики Ruby используют хэши

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

21
Рубин под микроскопом

пары значений должны храниться в хеше. Маленькие хэши довольно часто используются в приложениях Ruby —

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

Теория: как Ruby реализует хеш-


функции

Теперь давайте подробнее рассмотрим реальную хеш-функцию, которую

Ruby использует для назначения ключей и значений ячейкам в хеш-

таблицах. Если подумать, эта функция является центральной в

способ реализации объекта Hash - если эта функция работает хорошо, хэши Ruby будут быстрыми, но плохая хэш-функция

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

использует внутренние хеш-таблицы для хранения своей собственной информации, а не только значений данных, которые вы

сохраняете в хеш-объектах. Очевидно, что хорошая хэш-функция очень важна!

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

пару ключ/значение — в хеше, Ruby присваивает его ячейке внутри внутренней хеш-таблицы, используемой этим хеш-объектом:

22
Рубин под микроскопом

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

индекс бина = внутренняя_хэш_функция (ключ) % количество бинов

Или в этом примере:

2= хэш (: ключ) %11

Причина, по которой это хорошо работает для Ruby, заключается в том, что хеш-значения Ruby представляют собой более или менее случайные целые

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

этому:

$ ирб

ruby-1.9.3-p0:001 > "abc".хэш

=> 3277525029751053763

ruby-1.9.3-p0:002 > "абд".хэш

=> 234577060685640459

рубин-1.9.3-p0:003 > 1.хэш

=> -3466223919964109258

рубин-1.9.3-p0:004 > 2.хэш

=> -2297524640777648528

Здесь даже одинаковые значения имеют очень разные значения хеш-функции. Обратите внимание, что если я снова вызову «хеш», я всегда получаю одно и то

же целочисленное значение для одних и тех же входных данных:

ruby-1.9.3-p0:001 > "abc".хэш

=> 3277525029751053763

ruby-1.9.3-p0:002 > "абд".хэш

=> 234577060685640459

Вот как на самом деле работает хэш-функция Ruby для большинства объектов Ruby:

23
Рубин под микроскопом

• Когда вы вызываете «хэш», Ruby находит реализацию по умолчанию в классе «Объект». Вы, конечно,

можете переопределить это, если действительно хотите.

• Код C, используемый реализацией хэш-метода класса Object, получает значение указателя C для

целевого объекта, т. е. фактический адрес памяти структуры RValue этого объекта. По сути, это

уникальный идентификатор этого объекта.

• Затем Ruby пропускает его через сложную функцию C — хеш-функцию, которая смешивает и

перемешивает биты в значении, создавая повторяющееся псевдослучайное целое число.

Для строк и массивов это работает по-разному. В этом случае Ruby фактически выполняет итерацию по всем символам в

строке или элементам массива и вычисляет совокупное хеш-значение; это гарантирует, что хеш-значение всегда будет

одним и тем же для любого экземпляра строки или массива и всегда будет меняться при изменении любого из значений в

этой строке или массиве.

Наконец, целые числа и символы — еще один особый случай — для них Ruby просто передает их значения прямо в

хеш-функцию.

В Ruby 1.9 и 2.0 на самом деле используется хеш-функция MurmurHash, изобретенная Остином

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

в алгоритме: «умножить» и «повернуть». Если вас интересуют подробности того, как на самом

деле работает алгоритм Murmur, вы можете найти код C для него в файле исходного кода st.c

Ruby, около строки 1028. Или вы можете прочитать веб-страницу Остина на Murmur:http://

sites.google.com/site/murmurhash/ .

Кроме того, Ruby 1.9 и Ruby 2.0 инициализируют MurmurHash, используя случайное начальное значение, которое повторно

инициализируется при каждом перезапуске Ruby. Это означает, что если вы остановите и перезапустите Ruby, вы получите

разные хеш-значения для одних и тех же входных данных. Это также означает, что если вы попробуете это сами, вы получите

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

Ruby.

Поскольку хэш-значения являются псевдослучайными числами, как только Ruby разделит их на количество бинов,

например, 11, оставшиеся значения остатка (значения модуля) будут случайным числом от 0 до 10. Это означает,

что структуры st_table_entry будут равномерно распределяются по доступным бинам по мере их сохранения в хеш-

таблице. Равномерное распределение записей гарантирует, что Ruby сможет быстро искать и

24
Рубин под микроскопом

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

меньше максимальной плотности 5, которую я показал ранее.)

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

то же целое число для каждого значения входных данных. Что случилось бы?

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

тому же бину. Тогда Ruby получил бы все записи в одном длинном списке в этой одной корзине и без записей в какой-

либо другой корзине:

Теперь, когда вы попытаетесь получить какое-то значение из этого хеша, Ruby придется просмотреть этот длинный

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

будет очень и очень медленной.

25
Рубин под микроскопом

Эксперимент 4: Использование объектов в качестве ключей в хеше

Теперь я собираюсь доказать, что это так, и проиллюстрировать, насколько важной на

самом деле является хэш-функция Ruby, используя объекты с плохой хеш-функцией в

качестве ключей в хэше. Давайте повторим эксперимент 1 и создадим множество

хэшей с разным количеством элементов, от 1 до миллиона:

размер =2**экспонента

хэш = {}

(1..размер).каждый делать |н|

индекс = ранд

хэш[индекс] = ранд

конец

Но вместо того, чтобы вызывать «rand» для вычисления случайных значений ключей, на этот раз я создам новый пользовательский класс объектов с

именем «KeyObject» и буду использовать экземпляры этого класса в качестве значений ключей:

ключевой объект класса

конец

размер =2**экспонента

хэш = {}

(1..размер).каждый делать |н|

индекс = КлючевойОбъект.новый

хэш[индекс] = ранд

конец

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

каждого из этих объектов «KeyObject» вместо случайных значений с плавающей запятой, которые я использовал ранее.

После повторного запуска теста с этим классом KeyObject я перейду к изменению класса KeyObject и
переопределю метод «хэш», например:

26
Рубин под микроскопом

учебный класс ключевой объект

деф хэш

конец

конец

Я специально написал очень плохую хеш-функцию — вместо того, чтобы возвращать псевдослучайное целое число, эта

хеш-функция всегда возвращает целое число 4, независимо от того, для какого экземпляра объекта KeyObject вы ее

вызываете. Теперь Ruby всегда будет получать 4 при вычислении хеш-значения, и ему придется назначать все хэш-

элементы ячейке № 4 во внутренней хеш-таблице, как на диаграмме выше. Давай посмотрим что происходит….

Результаты: плохая хэш-функция сильно влияет на производительность!

Запуск теста с пустым классом KeyObject:

ключевой объект класса

конец

… Я получаю результаты, аналогичные эксперименту 1:

27
Рубин под микроскопом

Используя Ruby 1.9, я снова вижу, что Ruby требуется от 1,5 до 2 мс для извлечения 10 000 элементов из хеша, на этот раз с

использованием экземпляров класса KeyObject в качестве ключей.

Теперь давайте запустим тот же код, но на этот раз с плохой хэш-функцией в KeyObject:

учебный класс ключевой объект

деф хэш

4
конец

конец

Вот результаты:

28
Рубин под микроскопом

Вау - совсем другое! Обратите особое внимание на масштаб графика. По оси Y я показываю миллисекунды, а по оси X снова

количество элементов в хеше, показанное в логарифмическом масштабе. Но на этот раз обратите внимание, что у меня

есть 1000 миллисекунд - или фактических секунд - по оси Y! С 1 или небольшим количеством элементов я могу очень

быстро получить 10 000 значений — настолько быстро, что времени слишком мало, чтобы отобразить их на этом графике.

На самом деле это занимает примерно те же 1,5 мс.

Но когда количество элементов превышает 100 и особенно 1000, время, необходимое для загрузки 10 000

значений, увеличивается линейно с размером хэша. Для хеша, содержащего около 10 000 элементов, загрузка

10 000 значений занимает более 1,6 полных секунды. Если я продолжу тест с большими хэшами, загрузка значений

займет минуты или даже часы.

Опять же, здесь происходит то, что все элементы хэша сохраняются в одну и ту же корзину, заставляя Ruby выполнять

поиск по списку по одному ключу за раз.

29
Рубин под микроскопом

Теория: как Ruby сохраняет информацию о заказе в


хэшах

В моих диаграммах есть одна тонкая, но интересная деталь,


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

таблицы, как Ruby узнает, вставил ли я сначала :key или :key2?

Помимо оптимизации хэша в Ruby 2.0 как массива, еще одно важное изменение кода, которое я заметил между

различными версиями MRI Ruby, было связано с сохранением информации о порядке в хеш-таблице. Немного

покопавшись, я понял, что Ruby 1.8 и Ruby 1.9 ведут себя по-разному в отношении порядка и перебора хэш-

элементов. Я обнаружил, что в Ruby 1.9 и 2.0 структура st_table содержит два дополнительных значения данных,

называемых «голова» и «хвост»:

Значения head и tail являются указателями на структуры st_table_entry, формируя связанный список, который отслеживает

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

30
Рубин под микроскопом

Пунктирные линии обозначают связанный список. Когда первое значение добавляется в хеш-таблицу, указатели «голова»

и «хвост» указывают на него. Затем при добавлении второго значения формируется связанный список от заголовка до

первого значения и от первого до второго значения. Затем хвостовой указатель устанавливается на второе значение. Я не

показываю их здесь, но в структуру st_table_entry также добавлены новые указатели для поддержки связанного списка.

Зачем вся эта дополнительная работа? Связанный список позволяет Ruby 1.9 и Ruby 2.0 записывать порядок добавления

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

если я вызову один из методов, связанных с итератором, например «каждый», «каждое_значение» или «каждый_ключ» и т.

д., Ruby 1.9 и Ruby 2.0 будут искать указатель «head» в структуре st_table, а затем начните перебирать структуры

st_table_entry, используя указатели связанных списков.

31
Рубин под микроскопом

Ruby 1.8, однако, будет просто перебирать бины в массиве bin, а затем через структуры st_table_entry в том порядке, в

котором они будут найдены в хеш-таблице. Хотя этот порядок не является случайным - он связан со значением хеш-

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

возвращаете элементы во время итерации, также будет случайным.

Эксперимент 5: перебор элементов, вставленных в


хэш

Для этого теста мне просто нужно создать пустой хэш:

хэш = {}

… а затем вставьте в него два значения:

хэш ['один'знак равно"Это должно быть возвращено в первую очередь"

хэш ['два'знак равно"Это должно быть возвращено вторым"

… и, наконец, если я перебираю значения, я вижу, в каком порядке они возвращаются:

hash.each_value { |val| ставит значение }

Сначала запустим тест с использованием Ruby 1.8:

$ рубин -v

ruby 1.8.7 (30 июня 2011 г., уровень исправления 352) [i686-darwin11.2.0]

$ рубиновый эксперимент5.рб

Это должно быть возвращено вторым

Это должно быть возвращено в первую очередь

Здесь вы можете видеть, что на самом деле эти два элемента возвращаются внеправильныйпорядок: сначала

возвращается значение для :two, а затем значение для :one. Оказывается, для Ruby 1.8 хеш-значения для этих двух

ключей, «один» и «два», встречаются в неправильном порядке.

32
Рубин под микроскопом

Теперь давайте повторно запустим тот же код для Ruby 1.9 (или Ruby 2.0):

$ рубин -v

рубин 1.9.3p0 (30 октября 2011 г., редакция 33570) [x86_64-darwin11.2.0]

$ рубиновый эксперимент5.рб

Это должно быть возвращено в первую очередь

Это должно быть возвращено вторым

На этот раз я получаю значения в правильном порядке.

Помните, что для объекта Hash повторение ключей по порядку является второстепенной функцией. Наиболее важной

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

это делается путем вычисления хеш-значения и модуля по количеству ячеек, а не с помощью связанного списка начала/конца.

Ruby использует связанный список только тогда, когда вы вызываете «каждый» или один из других методов итератора. Ruby

также использует список при выполнении метода «shift».

Альтернативные теории: Хэши в JRuby

Оказывается, JRuby реализует хэши более или менее так же, как MRI Ruby.

Конечно, исходный код JRuby написан на Java, а не на C, но команда JRuby

решила использовать тот же базовый алгоритм хеш-таблицы, что и MRI.

Поскольку Java является объектно-ориентированным языком, в отличие от C,

JRuby может использовать фактические

Объекты Java для представления хеш-таблицы и записей хеш-таблицы вместо структур памяти.
Вот как выглядит хеш-таблица внутри процесса JRuby:

33
Рубин под микроскопом

Здесь вместо структур памяти C RHash и st_table у нас есть объект Java с именем «RubyHash». И вместо массива bin и

структур st_table_entry у нас есть массив объектов Java с именем «RubyHashEntry». Объект RubyHash содержит

переменную экземпляра с именем «размер», которая отслеживает количество элементов в хеше, и другую

переменную экземпляра с именем «таблица», которая представляет собой массив RubyHashEntry.

JRuby выделяет 11 пустых объектов RubyHashEntry при создании нового хеша; они образуют ячейки хеш-

таблицы. Затем, когда вы вставляете элементы в хэш, JRuby заполняет эти объекты ключами и значениями.

Вставка и извлечение элементов работает так же, как и в MRI: JRuby использует ту же формулу для деления хэш-

значения ключа на количество бинов и использует модуль, чтобы найти правильный бин:

индекс бина = внутренняя_хэш_функция (ключ) % количество бинов

По мере того, как вы добавляете в хэш все больше и больше элементов, JRuby формирует связанный список объектов RubyHashEntry по мере

необходимости, когда два ключа попадают в один и тот же бин — точно так же, как MRI:

34
Рубин под микроскопом

Кроме того, JRuby отслеживает плотность записей — среднее количество объектов RubyHashEntry в корзине — и при

необходимости выделяет большую таблицу объектов RubyHashEntry, перефразируя записи.

Если вам интересно, вы можете найти код Java, который JRuby использует для реализации

хэшей, в файле исходного кода src/org/jruby/RubyHash.java. Я обнаружил, что его легче понять,

чем исходный код C из MRI, в основном потому, что в целом Java немного более читабелен и

легче для понимания, чем C, и потому что он объектно-ориентирован. Команда JRuby смогла

разделить хеш-код на разные классы Java, прежде всего RubyHash и RubyHashEntry.

В некоторых случаях команда JRuby даже использовала те же имена идентификаторов, что и MRI; например, вы

найдете то же самое значение «ST_DEFAULT_MAX_DENSITY», равное 5, и JRuby использует ту же таблицу простых

чисел, что и MRI: 11, 19, 37 и т. д., которые попадают в степени двойки. Это означает, что JRuby будет

демонстрировать ту же модель производительности, что и MRI для перераспределения корзин и

перераспределения записей.

35
Рубин под микроскопом

Альтернативные теории: Хэши в Рубиниусе

На высоком уровне Rubinius использует тот же алгоритм хэш-таблицы, что и MRI и JRuby, но

использует Ruby вместо C или Java. Это означает, что исходный код Rubinius примерно в 10 раз

проще для понимания, чем код MRI или JRuby, и это отличный способ узнать больше о хеш-

таблицах, если вы заинтересованы в получении своих знаний.

руки грязные без изучения C или Java.

Вот как выглядят хеши внутри Rubinius:

Поскольку это обычный Ruby, в Rubinius ваши объекты Ruby на самом деле реализуются с помощью реального класса Ruby, который

называется «Hash». Вы увидите, что у него есть несколько целочисленных атрибутов, таких как @size, @capacity и @max_entries, а также

переменная экземпляра с именем @entries, представляющая собой массив бинов, фактически содержащий хэш-данные. Rubinius реализует

массив bin с помощью класса Ruby под названием «Rubinius::Tuple», который представляет собой простой класс хранения, похожий на

массив. Rubinius сохраняет каждый элемент хэша внутри объекта Ruby под названием «Bucket», сохраняемого внутри массива @entries

Rubinius::Tuple.

Одно отличие, которое вы увидите в реализации хэш-таблицы Rubinius, состоит в том, что она использует простые степени

двойки, чтобы решить, сколько хеш-бинов нужно создать, вместо простых чисел. Изначально Rubinius использует 16 объектов

Bucket. Всякий раз, когда Rubinius нужно выделить больше бинов, он просто удваивает размер массива бинов — «@entries» в

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

код, а также позволяет Рубиниусу использовать побитовую арифметику для вычисления индекса бина вместо необходимости

делить и брать остаток/модуль.

36
Рубин под микроскопом

Вы найдете реализацию хэшей Rubinius в файлах исходного кода с именами kernel/common/

hash18.rb и kernel/common/hash19.rb — Rubinius имеет совершенно разные реализации хэшей в

зависимости от того, начинаете ли вы в режиме совместимости с Ruby 1.8 или Ruby 1.9. Вот

фрагмент из hash18.rb, показывающий, как Rubinius находит значение по заданному ключу:

деф[](ключ)

если элемент = найти_элемент (ключ)

элемент.значение

еще

ключ по умолчанию

конец

конец

. . . так далее ...

# Ищет элемент, соответствующий +key+. Возвращает элемент

# если найдено. В противном случае возвращает +nil+.

дефнайти_элемент(ключ)

key_hash = ключ.хэш

пункт = @entries[key_index(key_hash)]

пока вещь

если элемент.соответствует? ключ, key_hash

вернуть изделие

конец

элемент = элемент.ссылка

конец

конец

. . . так далее ...

# Вычисляет слот +@entries + по заданному значению key_hash.

37
Рубин под микроскопом

дефключевой индекс(ключ_хэш)

key_hash и @маска

конец

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

индекса бина, поскольку количество бинов всегда будет степенью двойки для Rubinius, а не

простым числом. Поверьте мне, этот кодмноголегче понять, чем соответствующий код C в MRI

Ruby.

Вывод

Чем больше я смотрю на объект Ruby Hash, тем больше я впечатлен. На первый взгляд это кажется очень

очевидным и простым: вы вставляете значения и ключи, а позже можете получить их снова. Что может

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

знаний:

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

• Затем я понял, насколько на самом деле масштабируемы хэши Ruby. Теперь я могу написать код Ruby, который

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

любой заданный объект.

• Я видел, как основная команда Ruby улучшила — и продолжает улучшать — работу объекта Ruby Hash. Не могу

дождаться, чтобы увидеть, что они придумают дальше!

• Но самое главное, это было очень весело!

38
Рубин под микроскопом

Приложение. Код эксперимента

Эксперимент 1

Найдите этот код на Github.

требоватьэталон

ИТЕРАЦИИ = 10000

(1..20).каждый делать |показатель|

размер =2**экспонента

хэш = {}

целевой_индекс = 0

(1..размер).каждый делать |н|

индекс = ранд

хэш[индекс] = ранд

target_index = индекс, если n == размер/2

конец

GC.старт

Benchmark.bm сделать |bench|

скамейка.отчет("извлечение элемента из хэша с #{size} элементами #{ITERATIONS} раз") делать

ИТЕРАЦИЙ. раз сделать |n|

val = хэш[target_index]

конец

конец

конец

конец

39
Рубин под микроскопом

Эксперименты 2 и 3

Найдите этот код на Github.

ИТЕРАЦИИ = 10000

(0..99).каждый сделать |размер|

ставит"Создание хэшей #{ITERATIONS} с элементами #{size}."

хэши = []

ИТЕРАЦИЙ.раз делать

хэш = {}

(1..размер).каждый сделать |х|

хэш[ранд] = ранд

конец

хэш << хэш

конец

требоватьэталон

GC.старт

Benchmark.bm сделать |bench|

скамейка.отчет("добавление номера элемента #{size+1}") делать

ИТЕРАЦИЙ. раз сделать |n|

hashes[n][size] = ранд

конец

конец

конец

конец

40
Рубин под микроскопом

Эксперимент 4

Найдите этот код на Github.

требоватьэталон

ИТЕРАЦИИ =10000

учебный класс ключевой объект

деф хэш

4
конец

конец

(1..20).каждый делать |показатель степени|

размер =2**экспонента

хэш = {}

целевой_индекс = 0

(1..размер).каждый делать |н|

хэш_индекс = KeyObject.new

hash[hash_index] = ранд

target_index = hash_index, если n == размер/2

конец

GC.старт

Benchmark.bm сделать |bench|

скамейка.отчет("извлечение элемента из хэша с #{size} элементами #{ITERATIONS} раз") делать

ИТЕРАЦИЙ. раз сделать |n|

val = хэш[target_index]

конец

конец

конец

конец

41

Вам также может понравиться