Документация Engee

Оптимизации объединения 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 байт, чтобы учесть любую текущую переднюю буферизацию, которую может выполнять массив. Такая структура, в частности, позволяет очень эффективно изменять размеры, поскольку данные метки типа перемещаются только тогда, когда перемещаются данные самого массива.