Оптимизации объединения isbits
В Julia тип Array
хранит как битовые значения, так и выделенные в куче упакованные значения. Различие заключается в том, хранится ли само значение как встроенное (в непосредственно выделенной памяти массива), или же память массива является просто коллекцией указателей на объекты, выделенные в другом месте. С точки зрения производительности доступ к встроенным значениям является очевидным преимуществом по сравнению с необходимостью следовать указателю на фактическое значение. Определение isbits обычно означает любой тип Julia с фиксированным, детерминированным размером, что означает отсутствие полей указателей, см. ?isbitstype
.
Julia также поддерживает типы объединения, буквально — объединение набора типов. Пользовательские определения типов объединения могут быть чрезвычайно удобны для приложений, стремящихся охватить систему номинальных типов (т. е. явные отношения подтипов) и определить методы или функциональность для этих, иным образом не связанных, наборов типов. Однако задача компилятора заключается в том, чтобы определить способ обработки этих типов объединения. Собственный подход (и действительно то, что работало в Julia до версии 0.7) заключается в том, чтобы просто сделать ячейку, а затем указатель в ячейке на фактическое значение, аналогично ранее упомянутым упакованным значениям. Однако это неудачное решение, поскольку существует множество небольших примитивных типов битов (например, UInt8
, Int32
, Float64
и т. д.), которые легко поместились бы в эту ячейку, не требуя перенаправления для доступа к значению. В версии Julia 0.7 есть два основных способа, оптимизирующих этот подход: поля объединения isbits и массивы объединения isbits.
Структуры объединения isbits
Теперь Julia включает оптимизацию, при которой поля объединения isbits в типах (mutable struct
, struct
и т. д.) будут храниться как встроенные. Это достигается путем определения размера встраивания типа объединения (например, Union{UInt8, Int16}
будет иметь размер 2 байт, что представляет собой размер, необходимый для самого большого типа объединения Int16
), и выделения дополнительного байта метки типа (UInt8
), значение которого указывает на тип фактического значения, хранящегося как встроенное для байтов объединения. Значение байта метки типа является индексом типа фактического значения в порядке типов для типа объединения. Например, значение метки типа 0x02
для поля с типом Union{Nothing, UInt8, Int16}
указывает, что значение Int16
хранится в 16 битах поля в памяти структуры. Значение 0x01
указывает, что значение UInt8
хранится в первых 8 из 16 бит памяти поля. Наконец, значение 0x00
говорит о том, что для этого поля будет возвращено значение nothing
, несмотря на то, что, будучи одинарным типом с единственным экземпляром типа, оно технически имеет размер, равный 0. Байт метки типа для поля объединения типа хранится непосредственно в вычисляемой памяти объединения поля.
Массивы объединения isbits
Теперь Julia также может хранить значения объединения isbits как встроенные в массив в отличие от необходимости использования косвенной ячейки. Оптимизация достигается путем хранения дополнительного массива меток типов байтов, по одному байту на элемент массива, наряду с байтами данных фактического массива. Этот массив меток типов выполняет ту же функцию, что и регистр поля типа: его значение указывает на тип фактического хранимого значения объединения в массиве. С точки зрения структуры массив Julia может включать дополнительное буферное пространство до и после своих фактических значений данных, которые отслеживаются в полях a->offset
и a->maxsize
типа jl_array_t*
. Массив меток типов рассматривается точно так же, как еще один тип jl_array_t*
, но с теми же полями a->offset
, a->maxsize
и a->len
. Таким образом, формула для доступа к байтам меток типов для массива объединения isbits имеет вид a->data + (a->maxsize - a->offset) * a->elsize + a->offset
. Т. е. указатель массива a->data
уже сдвинут на a->offset
, поэтому, корректируя его, мы следуем за данными до максимума a->maxsize
, затем корректируем еще на a->offset
байт, чтобы учесть любую текущую переднюю буферизацию, которую может выполнять массив. Такая структура, в частности, позволяет очень эффективно изменять размеры, поскольку данные метки типа перемещаются только тогда, когда перемещаются данные самого массива.