Расположение объектов Julia в памяти

Структура объекта (jl_value_t)

Структура jl_value_t — это имя блока памяти, принадлежащего сборщику мусора Julia и представляющего данные, связанные с объектом Julia в памяти. При отсутствии информации о типе это просто непрозрачный указатель.

typedef struct jl_value_t* jl_pvalue_t;

Каждая структура jl_value_t содержится в структуре jl_typetag_t, которая содержит метаданные об объекте Julia, такие как его тип и доступность сборщику мусора.

typedef struct {
    opaque metadata;
    jl_value_t value;
} jl_typetag_t;

Типом любого объекта Julia является экземпляр конечного объекта jl_datatype_t. Для его запроса можно использовать функцию jl_typeof().

jl_value_t *jl_typeof(jl_value_t *v);

Структура объекта зависит от его типа. Для ее проверки можно использовать методы отражения. Доступ к полю можно получить, вызвав один из методов get-field.

jl_value_t *jl_get_nth_field_checked(jl_value_t *v, size_t i);
jl_value_t *jl_get_field(jl_value_t *o, char *fld);

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

jl_value_t *v = value->fieldptr[n];

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

struct {
    opaque metadata;
    struct {
        uint16_t data;        // -- 2 bytes
    } jl_value_t;
};

Этот объект создан с помощью jl_box_uint16(). Обратите внимание, что указатель jl_value_t ссылается на часть данных, а не на метаданные в верхней части структуры.

Во многих случаях значение может храниться неупакованным (только данные, без метаданных, или, возможно, даже не храниться, а просто находиться в регистрах), поэтому считать, что адрес контейнера является уникальным идентификатором, небезопасно. Для сравнения двух неизвестных объектов на эквивалентность следует использовать тест egal (идентичности) (соответствующий функции === в Julia).

int jl_egal(jl_value_t *a, jl_value_t *b);

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

Обратите внимание, что модификация указателя jl_value_t в памяти разрешена только в том случае, если объект является изменяемым. В противном случае изменение значения может нарушить работу программы и результат будет неопределенным. Свойство изменяемости значения может быть запрошено, как показано ниже.

int jl_is_mutable(jl_value_t *v);

Если сохраняемый объект является указателем jl_value_t, об этом также должен быть уведомлен сборщик мусора Julia.

void jl_gc_wb(jl_value_t *parent, jl_value_t *ptr);

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

Зеркальные структуры для некоторых встроенных типов определены в julia.h. Соответствующие глобальные объекты jl_datatype_t создаются с помощью jl_init_types в jltypes.c.

Биты метки сборщика мусора

Сборщик мусора использует несколько битов из части метаданных структуры jl_typetag_t для отслеживания каждого объекта в системе. Более подробную информацию об этом алгоритме можно найти в комментариях к реализации сборщика мусора в gc.c.

Выделение объектов

Большинство новых объектов выделяется с помощью jl_new_structv().

jl_value_t *jl_new_struct(jl_datatype_t *type, ...);
jl_value_t *jl_new_structv(jl_datatype_t *type, jl_value_t **args, uint32_t na);

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

jl_value_t *jl_new_bits(jl_value_t *bt, void *data)

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

Типы:

jl_datatype_t *jl_apply_type(jl_datatype_t *tc, jl_tuple_t *params);
jl_datatype_t *jl_apply_array_type(jl_datatype_t *type, size_t dim);

Хотя это наиболее часто используемые варианты, существуют и более низкоуровневые конструкторы, которые можно найти объявленными в julia.h. Они используются в jl_init_types() для создания начальных типов, необходимых для начальной загрузки образа системы Julia.

Кортежи:

jl_tuple_t *jl_tuple(size_t n, ...);
jl_tuple_t *jl_tuplev(size_t n, jl_value_t **v);
jl_tuple_t *jl_alloc_tuple(size_t n);

Представление кортежей является уникальным в экосистеме представлений объектов Julia. В некоторых случаях объект Base.tuple() может быть массивом указателей на объекты, содержащиеся в кортеже, эквивалентном следующему.

typedef struct {
    size_t length;
    jl_value_t *data[length];
} jl_tuple_t;

Однако в других случаях кортеж может быть преобразован в анонимный тип isbits и храниться неупакованным либо вообще не храниться (если он не используется в общем контексте как jl_value_t*).

Символы:

jl_sym_t *jl_symbol(const char *str);

Функции и MethodInstance:

jl_function_t *jl_new_generic_function(jl_sym_t *name);
jl_method_instance_t *jl_new_method_instance(jl_value_t *ast, jl_tuple_t *sparams);

Массивы:

jl_array_t *jl_new_array(jl_value_t *atype, jl_tuple_t *dims);
jl_array_t *jl_new_arrayv(jl_value_t *atype, ...);
jl_array_t *jl_alloc_array_1d(jl_value_t *atype, size_t nr);
jl_array_t *jl_alloc_array_2d(jl_value_t *atype, size_t nr, size_t nc);
jl_array_t *jl_alloc_array_3d(jl_value_t *atype, size_t nr, size_t nc, size_t z);
jl_array_t *jl_alloc_vec_any(size_t n);

Обратите внимание, что у многих из них есть альтернативные функции выделения для различных особых целей. В приведенном здесь списке указаны самые распространенные варианты использования. Более полный список можно найти, изучив файл заголовка julia.h.

В Julia хранилище обычно выделяется с помощью newstruct() (или newobj() для специальных типов).

jl_value_t *newstruct(jl_value_t *type);
jl_value_t *newobj(jl_value_t *type, size_t nfields);

На самом низком уровне память выделяется вызовом сборщика мусора (в gc.c), а затем помечается типом.

jl_value_t *jl_gc_allocobj(size_t nbytes);
void jl_set_typeof(jl_value_t *v, jl_datatype_t *type);

Обратите внимание, что все объекты выделяются кратно 4 байт и согласуются с размером указателя платформы. Память выделяется из пула для небольших объектов или напрямую с помощью функции malloc() для крупных объектов.

Singleton Types

Одинарные типы имеют только один экземпляр и не имеют полей данных. Отдельные экземпляры имеют размер 0 байт и состоят только из своих метаданных. Например, nothing::Nothing. См. разделы Одинарные типы и Пустые или отсутствующие значения.