【问题标题】:Stack allocation of `isbits` types in JuliaJulia 中 `isbits` 类型的堆栈分配
【发布时间】: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 中的内存管理的理解是:

  • 可变类型 -> 堆分配,
  • 不可变类型和元组 -> 堆栈分配,除非它们的字段之一是堆分配的(即可变的)。

但是,我不太了解这种行为的基本原理。我在某处读到,使用指向可变对象的指针分配堆栈不可变对象的问题在于,垃圾收集器可能会认为可变对象无法访问并过早地销毁它们。另一方面,如果我们将不可变对象放在堆上,那么仍然会有一个指向可变对象的指针,所以看起来我们避免了这个问题,但实际上我们只是将其转移到确保现在不可变对象本身不会被摧毁。

谁能给我解释一下这个对垃圾收集的工作原理只有很肤浅知识的人?

【问题讨论】:

  • 我们不会禁止递归类型,只是在类型定义时以可预测的方式计算它是“循环的”,并使类型的所有参数化都非内联。
  • 这也行得通,尽管听起来确实需要付出很多努力才能允许引用不可变类型。
  • 好吧,无论如何我们都需要一种方法来确定这一点,所以这只是抛出错误与不抛出错误之间的区别。
  • 而你现在需要两个字段来标记类型是否可变以及是否内联。基本上,关键是允许递归不可变会使规则更加复杂,我看不出我会从这种额外的复杂性中真正受益的地方。
  • 因为不可变类型对其他事情很有用,包括当您不小心写入您不想要的字段时捕获错误;让编译器提升负载,因为对象不会改变。我们也不缺少存储更多在运行时永远不会被修改的位所需的可忽略的内存量。

标签: garbage-collection julia


【解决方案1】:

引用其他对象的对象的堆栈分配问题是知道在垃圾收集期间需要跟踪它们。最简单的方法是 Julia 所做的:堆分配对象并使用与实际堆栈同步推送和弹出的“影子堆栈”“根”它们。这会引入相当多的开销并强制这些对象进行堆分配。

避免影子堆栈和堆分配开销的更复杂的方法是堆栈分配的这些对象,然后扫描执行垃圾收集的堆栈,并跟踪从堆栈中的对象到堆上的对象的引用。但是,这需要知道堆栈中的哪些对象是指向堆上对象的指针——通常,不能保证非堆分配的对象在寄存器或堆栈中保持完整或连续。这样做的一种方法称为“保守堆栈扫描”,它需要在 gc 期间假设堆栈上的任何值看起来都可能是指向堆上对象的指针。这种方法已成功用于 Safari 的 JavaScript 引擎等应用程序,但也并非没有挑战。我们曾考虑在 Julia 中使用保守的堆栈扫描,并开始尝试这样做,但从未完成。

参考资料:

【讨论】:

    【解决方案2】:

    每当提出这个问题时,就会有多个问题/概念经常混在一起。

    1. 可变或非无指针不可变并不一定意味着堆分配,我们已经有优化通道来省略一些优化,并且正在努力进一步改进它们。

    2. 对象布局 ABI 是用户可见的行为,不是优化过程可以轻易改变的东西(除非它可以证明它想要做的局部优化不会逃脱)。当前的 ABI 是只有不可变的 isbits 将被内联存储(并且在用作局部变量时“堆栈分配”)。提高内联对象的无指针要求有一个基本限制,即处理递归类型的必要性。将引用循环中的所有类型都内联存储是不可能的,如果我们想让其中一些内联,就必须在某个地方中断循环。我相信我们确实有一个一致且可预测的模型来做到这一点,尽管这是否可取是另一个问题。

      这在某种程度上与性能有关,但并非总是如此。内联存储意味着更多的副本,因此如果我们进行切换,很难确保没有回归。

      编辑:我还应该提到,无指针是无循环的充分条件并且更容易计算,这也是我们目前使用它来打破内联循环的部分原因。

    3. GC 支持。这基本上是最简单的部分。让 GC 识别堆栈上的指针非常容易。如果我们决定更改对象布局 ABI,就需要这样做。

      编辑:我应该补充一点,“GC 支持”是必需的,因为我们目前只支持有限/简单的堆栈布局用于对象引用(即指针数组)。这就是需要改进的地方。

    【讨论】:

    • “很容易”,对吧?这是否意味着您即将发布补丁? :D
    • 这意味着我们从 Oscar 那里得到了一个工作版本,在 GC 对此感到满意后,他大部分时间都在努力处理 codegen....
    • 或者换句话说,除非 codegen 能够很好地利用它,否则这种支持是毫无用处的。因此,如果有人打算实现这一目标并需要 GC 的帮助,我肯定会这样做。不过,基诺似乎已经掌握了它;-p
    • 我并没有真正看到周期的问题。一组不可变对象必须形成一个无环图,因为所有边都必须从新到旧指向。
    • 问题是计算布局。虽然确实必须构造每个不可变对象,因此它们必须在某个地方终止,但您不能使它们都具有不同的布局(或者更确切地说,我们不想这样做)。换句话说,给定struct A x::A; A() = new(); A(a) = new(a); endA()A(A())A(A(A()))等必须具有相同的大小。
    猜你喜欢
    • 1970-01-01
    • 2011-02-18
    • 1970-01-01
    • 2012-06-26
    • 2011-10-06
    • 1970-01-01
    • 2014-10-11
    • 1970-01-01
    • 2012-06-05
    相关资源
    最近更新 更多