【发布时间】:2017-05-12 08:15:30
【问题描述】:
问题和答案摘要
特定类型的对象,比如说
type Foo
a::A
b::B
end
可以通过以下两种方式之一存储:
内联(又名按值):在这种情况下,语句“变量
foo::Foo存储在位置x”实际上意味着我们在位置x有一个变量foo.a::A和一个变量@ 987654326@x + sizeof(A)位置(技术上地址可能有点复杂,但这与我们的目的无关)。引用(也称为引用):“
foo::Foo存储在位置x”表示位置x包含一个指针fooptr::Ptr{Foo},因此在位置@987654333 处有一个变量foo.a::A@ 和foo.b::B在位置fooptr + sizeof(A)。
与其他语言不同(我在看你,C/C++),Julia 自行决定是存储内联变量还是引用变量,并且它根据类型的属性这样做:
- 可变类型 -> 引用,
- 不可变类型 -> 如果至少有一个字段被引用,则被引用,否则被内联。
这条规则至少有两个原因:
StefanKarpinski 的回答:垃圾收集器需要能够找到指向堆栈上堆分配对象的所有指针。目前,Julia 通过将所有此类指针存储在单独的“影子堆栈”中来确保这一点,但如果我们允许将包含指针的复合类型放置在堆栈中,那么这种巧妙的分离将不再可能。相反,编译器需要在其他变量中寻找指针,这会带来技术上的困难。
-
yuyichao 的回答:Julia 要求在每个类型而不是每个对象的基础上做出内联/引用决定,这意味着假设类型
immutable A a::A end如果我们坚持内联它,它必须无限大。所以我们要么必须禁止这种递归不可变类型,要么我们最多可以允许内联非递归不可变类型。
原始问题
我对 Julia 中的内存管理的理解是:
- 可变类型 -> 堆分配,
- 不可变类型和元组 -> 堆栈分配,除非它们的字段之一是堆分配的(即可变的)。
但是,我不太了解这种行为的基本原理。我在某处读到,使用指向可变对象的指针分配堆栈不可变对象的问题在于,垃圾收集器可能会认为可变对象无法访问并过早地销毁它们。另一方面,如果我们将不可变对象放在堆上,那么仍然会有一个指向可变对象的指针,所以看起来我们避免了这个问题,但实际上我们只是将其转移到确保现在不可变对象本身不会被摧毁。
谁能给我解释一下这个对垃圾收集的工作原理只有很肤浅知识的人?
【问题讨论】:
-
我们不会禁止递归类型,只是在类型定义时以可预测的方式计算它是“循环的”,并使类型的所有参数化都非内联。
-
这也行得通,尽管听起来确实需要付出很多努力才能允许引用不可变类型。
-
好吧,无论如何我们都需要一种方法来确定这一点,所以这只是抛出错误与不抛出错误之间的区别。
-
而你现在需要两个字段来标记类型是否可变以及是否内联。基本上,关键是允许递归不可变会使规则更加复杂,我看不出我会从这种额外的复杂性中真正受益的地方。
-
因为不可变类型对其他事情很有用,包括当您不小心写入您不想要的字段时捕获错误;让编译器提升负载,因为对象不会改变。我们也不缺少存储更多在运行时永远不会被修改的位所需的可忽略的内存量。