Шаблоны на объекте Fixnum в Ruby?
Идентификатор объекта 0 равен 1, 1 равен 3, 2 равен 5.
Почему этот шаблон такой? Что стоит за Fixnums, которые создают этот шаблон object_ids? Я ожидаю, что если 0 имеет идентификатор 1, 1 имеет идентификатор 2, 2 имеет идентификатор 3.. И так далее.
Что мне не хватает?
2 ответа
Перво-наперво: единственное, что гарантирует языковая спецификация Ruby object_id
Это то, что они уникальны в космосе. Вот и все. Они даже не уникальны во времени.
Таким образом, в любой момент времени может быть только один объект с определенным object_id
в то же время, однако, в разное время, object_id
s могут быть повторно использованы для разных объектов.
Чтобы быть точным: Ruby гарантирует, что
object_id
будетInteger
- никакие два объекта не будут одинаковыми
object_id
в то же время - объект будет иметь то же самое
object_id
в течение всей своей жизни
То, что вы видите, является побочным эффектом того, как object_id
а также Fixnum
S реализованы в YARV. Это частная внутренняя деталь реализации YARV, которая никак не гарантируется. Другие реализации Ruby могут (и делают) реализовывать их по-разному, так что это не гарантируется для всех реализаций Ruby. Это даже не гарантируется для разных версий YARV или даже для одной и той же версии на разных платформах.
И на самом деле, это действительно изменилось совсем недавно, и это отличается между 32-битной и 64-битной платформами.
В ЯРВ, object_id
просто реализуется как возвращение адреса памяти объекта. Это одна часть головоломки.
Гайка, почему адреса памяти Fixnum
так регулярно? Ну, на самом деле, в этом случае, это не адреса памяти! YARV использует специальный трюк для кодирования некоторых объектов в указатели. Есть некоторые указатели, которые на самом деле не используются, и поэтому вы можете использовать их для кодирования определенных вещей.
Это называется представлением тегового указателя и является довольно распространенным приемом оптимизации, который использовался многими интерпретаторами, виртуальными машинами и системами времени выполнения в течение десятилетий. Практически каждая реализация Lisp использует их, много виртуальных машин Smalltalk, много интерпретаторов Ruby и так далее.
Обычно в этих языках вы всегда передаете указатели на объекты. Сам объект состоит из заголовка объекта, который содержит метаданные объекта (например, тип объекта, его класс (ы), возможно, ограничения контроля доступа или аннотации безопасности и т. Д.), А затем сами данные объекта. Таким образом, простое целое число будет представлено как указатель плюс объект, состоящий из метаданных и фактического целого числа. Даже с очень компактным представлением это что-то вроде 6 байт для простого целого числа.
Кроме того, вы не можете передать такой целочисленный объект в CPU, чтобы выполнить быструю целочисленную арифметику. Если вы хотите добавить два целых числа, у вас действительно есть только два указателя, которые указывают на начало заголовков объектов двух целочисленных объектов, которые вы хотите добавить. Итак, сначала вам нужно выполнить целочисленную арифметику с первым указателем, чтобы добавить смещение в объект, в котором хранятся целочисленные данные. Тогда вы должны разыменовать этот адрес. Сделайте то же самое снова со вторым целым числом. Теперь у вас есть два целых числа, которые вы действительно можете попросить ЦП добавить. Конечно, теперь вам нужно создать новый целочисленный объект для хранения результата.
Таким образом, чтобы выполнить одно целочисленное сложение, вам фактически нужно выполнить три целочисленных сложения плюс две разыменования указателя и одну конструкцию объекта. И ты занимаешь почти 20 байтов.
Однако хитрость заключается в том, что с так называемыми неизменяемыми типами значений, такими как целые числа, вам обычно не нужны все метаданные в заголовке объекта: вы можете просто оставить все эти вещи и просто синтезировать их (то есть VM-nerd- говорят "подделка"), когда кто-нибудь хочет посмотреть. Фикснум всегда будет иметь класс Fixnum
нет необходимости отдельно хранить эту информацию. Если кто-то использует отражение, чтобы выяснить класс Fixnum, вы просто отвечаете Fixnum
и никто никогда не узнает, что вы на самом деле не сохранили эту информацию в заголовке объекта и что фактически даже нет заголовка объекта (или объекта).
Таким образом, хитрость заключается в том, чтобы хранить значение объекта в указателе на объект, эффективно объединяя два в один.
Существуют ЦП, которые на самом деле имеют дополнительное пространство внутри указателя (так называемые биты тега), которые позволяют хранить дополнительную информацию о указателе внутри самого указателя. Дополнительная информация типа "это на самом деле не указатель, а целое число". Примерами могут служить Burroughs B5000, различные машины Lisp или AS/400. К сожалению, большинство современных основных процессоров не имеют этой функции.
Однако выход есть: большинство современных процессоров работают значительно медленнее, когда адреса не выровнены по границам слов. Некоторые даже не поддерживают неприсоединенный доступ вообще.
Это означает, что на практике все указатели будут делиться на 4 (в 32-битной системе, 8 в 64-битной системе), что означает, что они всегда заканчиваются двумя (три в 64-битной системе) 0
биты. Это позволяет нам различать реальные указатели (которые заканчиваются на 00
) и указатели, которые на самом деле являются замаскированными целыми числами (те, которые заканчиваются на 1
). И это все еще оставляет нас со всеми указателями, которые заканчиваются 10
свободно делать другие вещи. Кроме того, большинство современных операционных систем резервируют для себя очень низкие адреса, что дает нам еще одну область, с которой нужно возиться (указатели начинаются, скажем, с 24 0
с и заканчивается 00
).
Таким образом, вы можете закодировать 31-битное (или 63-битное) целое число в указатель, просто сместив его на 1 бит влево и добавив 1
к этому. И вы можете выполнять очень быструю целочисленную арифметику с ними, просто смещая их соответствующим образом (иногда даже это не нужно).
Что мы делаем с этими другими адресными пространствами? Ну, типичные примеры включают кодирование float
S в другом большом адресном пространстве и ряде специальных объектов, таких как true
, false
, nil
127 символов ASCII, некоторые часто используемые короткие строки, пустой список, пустой объект, пустой массив и т. д. рядом с 0
адрес.
В YARV целые числа кодируются так, как я описал выше, false
кодируется как адрес 0
(что так же бывает и представление false
в с), true
как адрес 2
(что именно так происходит в представлении C true
сдвинут на один бит) и nil
как 4
,
В YARV следующие кодировки используются для кодирования определенных специальных объектов:
xxxx xxxx … xxxx xxx1 Fixnum
xxxx xxxx … xxxx xx10 flonum
0000 0000 … 0000 1100 Symbol
0000 0000 … 0000 0000 false
0000 0000 … 0000 1000 nil
0000 0000 … 0001 0100 true
0000 0000 … 0011 0100 undefined
Fixnum
s - это 63-битные целые числа, которые вписываются в одно машинное слово, flonum
62-битные Float
которые вписываются в одно машинное слово. false
, nil
а также true
это то, что вы ожидаете, undefined
это значение, которое используется только внутри реализации, но не предоставляется программисту.
Обратите внимание, что на 32-битных платформах flonum
s не используются (нет смысла использовать 30-битный Float
s) и поэтому битовые комбинации различны. nil.object_id
является 4
на 32-битных платформах, а не 8
как на 64-битных платформах, например.
Итак, вот оно:
- определенные маленькие целые числа кодируются как указатели
- указатели используются для
object_id
s
Следовательно
- определенные маленькие целые имеют предсказуемые
object_id
s
Для Fixnum i
идентификатор_объекта i * 2 + 1
,
Для object_id из 0, 2, 4
что они? Они есть false
, true
, nil
в рубине.