Memory layout of Julia Objects
Object layout (jl_value_t
)
jl_value_t
结构是由 Julia 垃圾收集器拥有的一块内存的名称,表示与内存中 Julia 对象相关的数据。在没有任何类型信息的情况下,它只是一个不透明指针:
typedef struct jl_value_t* jl_pvalue_t;
每个 jl_value_t
结构体包含在一个 jl_typetag_t
结构体中,该结构体包含关于 Julia 对象的元数据,例如其类型和垃圾收集器 (gc) 可达性:
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);
对象的布局取决于其类型。可以使用反射方法来检查该布局。可以通过调用其中一个获取字段的方法来访问字段:
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
指针引用的是数据部分,而不是结构体顶部的元数据。
一个值在许多情况下可以“未包装”存储(仅数据,没有元数据,可能甚至不存储而只是保留在寄存器中),因此假设一个盒子的地址是唯一标识符是不安全的。应该使用“平等”测试(对应于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);
然而,手册的 Embedding Julia 部分在此时也是必读内容,因为它涵盖了各种类型的装箱和拆箱的其他细节,以及理解垃圾回收的交互。
镜像结构体对于一些内置类型是 defined in julia.h
。相应的全局 jl_datatype_t
对象是通过 jl_init_types
in jltypes.c
创建的。
Garbage collector mark bits
垃圾收集器使用 jl_typetag_t
的元数据部分中的几个位来跟踪系统中的每个对象。有关此算法的更多详细信息,请参见 garbage collector implementation in gc.c
的注释。
Object allocation
大多数新对象是通过 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);
函数和方法实例:
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_alloc_array_1d(jl_value_t *atype, size_t nr);
jl_array_t *jl_alloc_array_nd(jl_value_t *atype, size_t *dims, size_t ndims);
请注意,这些中有许多具有各种特殊用途的替代分配函数。这里的列表反映了更常见的用法,但可以通过阅读 julia.h
header file 找到更完整的列表。
在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);
函数 jl_gc_allocobj
的文档和用法可能已经过时。
请注意,所有对象都是以4字节的倍数分配,并与平台指针大小对齐。内存是从池中为较小的对象分配的,或者对于较大的对象直接使用malloc()
分配。