在Julia中堆栈分配`isbits`类型

gTc*_*TcV 4 garbage-collection julia

问答摘要

比方说,特定类型的对象

type Foo
    a::A
    b::B
end
Run Code Online (Sandbox Code Playgroud)

可以用以下两种方式之一存储:

  • 内联(又名按值):在这种情况下,声明"变量foo::Foo被存储在位置x"实际上意味着我们有一个变量foo.a::A在位置x和可变foo.b::B在位置x + sizeof(A)(技术上的地址可能是更复杂一点,但是这无关紧要我们的目的).

  • 引用(也称为引用):" foo::Foo存储在位置x"意味着该位置x包含指针fooptr::Ptr{Foo},使得foo.a::A在位置fooptrfoo.b::B位置处存在变量fooptr + sizeof(A).

与其他语言不同(我正在看你C/C++),Julia自己决定是否存储内联或引用的变量,并且它是基于类型的属性来实现的:

  • 可变类型 - >引用,
  • 不可变类型 - >如果引用了至少一个字段,则引用,否则为内联.

此规则至少有两个原因:

  • StefanKarpinski的回答:垃圾收集器需要能够找到堆栈上堆分配对象的所有指针.目前,Julia通过将所有这些指针存储在单独的"影子堆栈"上来确保这一点,但是如果我们允许将包含指针的复合类型放置在堆栈上,那么这样的整齐分离将不再可能.相反,编译器需要在其他变量中寻找引起技术困难的指针.

  • yuyichao的回答:Julia要求内联/引用决策是基于每个类型而不是每个对象,这意味着假设类型

    immutable A
        a::A
    end
    
    Run Code Online (Sandbox Code Playgroud)

    如果我们坚持内联它就必须是无限大的.所以我们要么禁止这样的递归不可变类型,要么我们最多允许内联非递归不可变类型.


原始问题

我对Julia的内存管理的理解是:

  • 可变类型 - >堆分配,
  • 不可变类型和元组 - > stack-allocated,除非它们的一个字段是堆分配的(即可变的).

但是,我不太明白这种行为的基本原理.我已经读过一些地方,堆栈分配不可变的问题与指向mutables的指针是垃圾收集器可能认为mutables无法访问并过早地破坏它们.另一方面,如果我们将不可变置于堆上,那么仍然会有一个指向mutable的指针,所以看起来我们可以避免这个问题,但实际上我们只是将它转移到确保现在不可变本身不会被摧毁.

任何人都能向我解释一下,他对垃圾收集的工作原理只有非常肤浅的知识吗?

Ste*_*ski 6

引用其他对象的对象的堆栈分配问题是知道在垃圾收集期间需要跟踪它们.执行此操作的最简单方法是Julia所做的事情:堆分配对象并使用"影子堆栈"将它们"root",并将其与实际堆栈同步推送和弹出.这会引入相当大的开销并迫使这些对象进行堆分配.

避免影子堆栈和堆分配开销的更复杂方法是​​堆栈分配这些对象,然后扫描执行垃圾收集的堆栈,并跟踪堆栈中对象的引用到堆上的对象.但是,这需要知道堆栈中的哪些对象是指向堆上对象的指针 - 通常,不保证非堆分配的对象在寄存器或堆栈中保持完整或连续.执行此操作的一种方法称为"保守堆栈扫描",其需要在gc期间假设堆栈上看起来像它的任何值实际上是指向堆上的对象的指针.这种方法已成功用于Safari的JavaScript引擎等应用程序,但它并非没有挑战.我们已经考虑过在Julia中使用保守的堆栈扫描,并且已经开始了这样做的初步努力但是努力从未完成.

参考文献:


yuy*_*hao 5

有很多问题/概念经常在提出时混合在一起.

  1. mutable或non-pointerfree immutable并不一定意味着堆分配,我们已经有优化传递来避免一些优化,并且正在努力进一步改进它们.

  2. 对象布局ABI是用户可见的行为,而不是优化传递可以轻易改变的东西(除非它可以证明它想要做的局部优化不会逃脱).当前的ABI是只有isbits immutable将被内联存储(当用作局部变量时,"stack allocated").提升内联对象的指针自由度的要求是一个基本限制,即处理递归类型的必要性.不可能将所有类型都存储在内联存储的参考圆中,如果我们想让它们中的一些内联,则必须在某个地方打破循环.我相信我们确实有一个一致且可预测的模型来做到这一点,尽管这是否是可取的是另一个问题.

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

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

  3. GC支持.这基本上是最容易的部分.使GC识别堆栈上的指针非常容易.如果我们决定更改对象布局ABI,则需要完成.

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

  • "很容易",对吧?这是否意味着你即将获得补丁?:d (2认同)
  • 问题是计算布局.虽然确实每个不可变的都必须被构造,因此它们必须终止某个地方,你不能让它们都有不同的布局(或者更确切地说,我们不想).换句话说,给出`struct A x :: A; A()= new(); A(a)= new(a); 结束``A()`,`A(A())`和`A(A(A()))`等都必须具有相同的大小. (2认同)