Ответ 1
Это называется помеченным представлением указателя и является довольно распространенным трюком оптимизации, используемым во многих разных интерпретаторах, виртуальных машинах и системах исполнения на протяжении десятилетий. Практически каждая реализация Lisp использует их, многие Smalltalk VM, многие интерпретаторы Ruby и т.д.
Обычно на этих языках вы всегда передаете указатели на объекты. Сам объект состоит из заголовка объекта, который содержит метаданные объекта (такие как тип объекта, его класс (ы), возможно ограничения доступа или аннотации безопасности и т.д.), А затем сами фактические данные объекта. Таким образом, простое целое число будет представлено как указатель плюс объект, состоящий из метаданных и фактического целого числа. Даже с очень компактным представлением, что-то вроде 6 байтов для простого целого.
Кроме того, вы не можете передать такой целочисленный объект в CPU для выполнения быстрой целочисленной арифметики. Если вы хотите добавить два целых числа, у вас действительно есть только два указателя, которые указывают на начало заголовков объектов двух целых объектов, которые вы хотите добавить. Итак, сначала нужно выполнить целочисленную арифметику на первом указателе, чтобы добавить смещение в объект к нему, где хранятся целочисленные данные. Тогда вам придется разыгрывать этот адрес. Сделайте то же самое со вторым целым числом. Теперь у вас есть два целых числа, которые вы можете попросить добавить. Конечно, вам нужно построить новый целочисленный объект для хранения результата.
Итак, чтобы выполнить одно целочисленное добавление, вам действительно нужно выполнить три целых дополнения плюс два указателя с указателем и одну конструкцию объекта. И вы занимаете почти 20 байт.
Однако фокус в том, что с так называемыми неизменяемыми типами значений, такими как целые числа, вам обычно не нужны все метаданные в заголовке объекта: вы можете просто оставить все это и просто синтезировать его (что является виртуальной машиной) -нерд-говорить за "подделку" ), когда кому-то нужно смотреть. Целое число всегда будет иметь класс Integer
, нет необходимости отдельно хранить эту информацию. Если кто-то использует рефлексию для определения класса целого числа, вы просто отвечаете Integer
, и никто никогда не узнает, что вы фактически не сохранили эту информацию в заголовке объекта, и на самом деле нет даже заголовка объекта (или объект).
Итак, трюк заключается в том, чтобы сохранить значение объекта внутри указателя на объект, эффективно сворачивая два на один.
Существуют процессоры, которые на самом деле имеют дополнительное пространство внутри указателя (так называемые биты тегов), которые позволяют хранить дополнительную информацию о указателе внутри самого указателя. Дополнительная информация типа "на самом деле это не указатель, это целое число". Примеры включают Burroughs B5000, различные машины Lisp или AS/400. К сожалению, большинство современных процессоров не имеют этой функции.
Однако есть выход: большинство современных основных процессоров работают значительно медленнее, когда адреса не выравниваются по границам слов. Некоторые даже не поддерживают неприсоединенный доступ вообще.
Это означает, что на практике все указатели будут делиться на 4, что означает, что они всегда будут заканчиваться двумя битами 0
. Это позволяет нам различать реальные указатели (заканчивающиеся на 00
) и указатели, которые на самом деле являются целыми числами (те, которые заканчиваются на 1
). И это все равно оставляет нас со всеми указателями, которые заканчиваются на 10
, чтобы делать другие вещи. Кроме того, большинство современных операционных систем резервируют очень низкие адреса для себя, что дает нам еще одну область, с которой можно столкнуться (указатели, начинающиеся с, скажем, 24 0
и заканчивающиеся на 00
).
Итак, вы можете закодировать 31-битное целое число в указатель, просто сдвинув его на 1 бит влево и добавив к нему 1
. И вы можете выполнить очень быструю целочисленную арифметику с ними, просто переместив их соответствующим образом (иногда даже не обязательно).
Что мы будем делать с этими другими адресными пространствами? Ну, типичные примеры включают кодирование float
в другом большом адресном пространстве и ряд специальных объектов, таких как true
, false
, nil
, 127 символов ASCII, некоторые обычно используемые короткие строки, пустой список, пустой объект, пустой массив и т.д. рядом с адресом 0
.
Например, в интерпретаторах MRI, YARV и Rubinius Ruby целые числа кодируются так, как я описал выше, false
кодируется как адрес 0
(как раз так происходит и представление false
в C), true
в качестве адреса 2
(как это обычно бывает, представление C из true
сдвинуто на один бит) и nil
как 4
.