垃圾收集器如何从堆栈中找出对象引用?

huangapple go评论89阅读模式
英文:

How can a garbage collector find out about object references done from the stack?

问题

在像Haskell或Go这样具有自动垃圾回收的语言中,垃圾收集器如何确定存储在堆栈上的值是指向内存的指针还是仅仅是数字?如果垃圾收集器只是扫描堆栈并假设所有地址都是对象的引用,那么很多对象可能会被错误地标记为可达。

显然,可以在每个堆栈帧的顶部添加一个值,描述接下来的多少个值是指针,但这样做会带来很大的性能开销吗?

实际上是如何实现的?

英文:

In languages with automatic garbage collection like Haskell or Go, how can the garbage collector find out which values stored on the stack are pointers to memory and which are just numbers? If the garbage collector just scans the stack and assumes all addresses to be references to objects, a lot of objects might get incorrectly marked as reachable.

Obviously, one could add a value to the top of each stack frame that described how many of the next values are pointers, but wouldn't that cost a lot of performance?

How is it done in reality?

答案1

得分: 20

一些收集器假设堆栈上的所有内容都是潜在的指针(如Boehm GC)。事实证明,这并不像人们可能期望的那样糟糕,但显然是次优的。在受管理的语言中,通常会在堆栈上保留一些额外的标记信息,以帮助收集器确定指针的位置。

请记住,在大多数编译语言中,每次进入函数时,堆栈帧的布局都是相同的,因此很容易确保以正确的方式标记数据。

“位图”方法是一种实现这一目标的方法。位图的每一位对应堆栈上的一个字。如果位为1,则堆栈上的位置是指针;如果位为0,则该位置从收集器的角度来看只是一个数字(或类似的内容)。非常出色的GHC运行时和调用约定使用大多数函数的一字布局,其中几个位用于表示堆栈帧的大小,其余位用作位图。较大的堆栈帧需要多字结构,但思想是相同的。

关键是开销很低,因为布局信息在编译时计算,然后在每次调用函数时包含在堆栈中。

更简单的方法是“指针优先”,其中所有指针位于堆栈的开头。您只需要在指针之前包含一个长度,或者在指针之后包含一个特殊的“结束”字,以告知给定此布局的哪些字是指针。

有趣的是,将此管理信息放入堆栈中会产生与C的互操作相关的一系列问题。例如,将高级语言编译为C是次优的,因为尽管C是可移植的,但很难携带此类信息。针对C类似语言(如GCC、LLVM)设计的优化编译器可能会重组堆栈帧,从而产生问题,因此GHC LLVM后端使用自己的“堆栈”而不是LLVM堆栈,这会导致一些优化损失。同样,C代码和“受管理”代码之间的边界需要谨慎构建,以避免混淆垃圾收集器。

因此,当在JVM上创建新线程时,实际上会创建两个堆栈(一个用于Java,一个用于C)。

英文:

Some collectors assume everything on the stack is a potential pointer (like Boehm GC). This turns out to be not as bad as one might expect, but is clearly suboptimal. More often in managed languages, some extra tagging information is left with the stack to help the collector figure out where the pointers are.

Remember that in most compiled languages, the layout of a stack frame is the same every time you enter a function, therefore it is not that hard to ensure that you tag your data in the right way.

The "bitmap" approach is one way of doing this. Each bit of the bitmap corresponds to one word on the stack. If the bit is a 1 then the location on the stack is a pointer, and if it is a 0 then the location is just a number from the point of view of the collector (or something along those lines). The exceptionally well written GHC runtime and calling conventions use a one word layout for most functions, such that a few bits communicate the size of the stack frame, with the rest serving as the bitmap. Larger stack frames need a multi word structure, but the idea is the same.

The point is that the overhead is low, since the layout information is computed at compile time, and then included in the stack every time a function is called.

An even simpler approach is "pointer first", where all the pointers are located at the beginning of the stack. You only need to include a length prior to the pointers, or a special "end" word after them, to tell which words are pointers given this layout.

Interestingly, trying to get this management information on to the stack produces a host of problem related to interop with C. For example, it is sub optimal to compile high level languages to C, since even though C is portable, it is hard to carry this kind of information. Optimizing compilers designed for C like languages (GCC,LLVM) may restructure the stack frame, producing problems, so the GHC LLVM backend uses its own "stack" rather than the LLVM stack which costs it some optimizations. Similarly, the boundary between C code, and "managed" code needs to be constructed carefully to keep from confusing the GC.

For this reason, when you create a new thread on the JVM you actually create two stacks (one for Java, one for C).

答案2

得分: 16

Haskell stack在每个堆栈帧中使用一个字的内存,用位图描述该堆栈帧中的哪些值是指针,哪些不是。有关详细信息,请参阅GHC Commentary中的“堆栈布局”文章和“位图布局”文章。

公平地说,考虑到一切,一个字的内存真的不算太多成本。你可以将其视为每个方法添加一个变量;这并不是太糟糕。

英文:

The Haskell stack uses a single word of memory in each stack frame describing (with a bitmap) which of the values in that stack frame are pointers and which are not. For details, see the "Layout of the stack" article and the "Bitmap layout" article from the GHC Commentary.

To be fair, a single word of memory really isn't much cost, all things considered. You can think of it as just adding a single variable to each method; that's not all that bad.

答案3

得分: 11

存在一些垃圾收集器(GC),它们假设每个位模式都是GC正在管理的某个东西的地址,并且不会释放该东西。这实际上可以工作得很好,因为指针通常比小的常见整数大,并且通常需要对齐。但是是的,这可能会导致某些对象的收集被延迟。C语言的Boehm收集器就是这样工作的,因为它是基于库的,所以不会从编译器获得任何特定的帮助。

还有一些与它们所使用的语言更紧密耦合的GC,实际上了解内存中对象的结构。我从未详细了解过堆栈帧处理,但如果编译器和GC设计为协同工作,您可以记录信息来帮助GC。一个技巧是将所有指针引用放在一起,并使用每个堆栈帧的一个字来记录引用的数量,这不会带来太大的开销。如果您可以在不添加额外字的情况下确定每个堆栈帧对应的函数,则可以编译一个每个函数的“堆栈帧布局映射”。另一个选择是使用标记字,其中将非指针的低位设置为1,而指针由于地址对齐永远不需要这样做,因此可以区分它们。这意味着您必须对非装箱值进行移位才能使用它们。

英文:

There exist GCs that assume that every bit pattern that is the address of something the GC is managing is in fact a pointer (and so don't release the something). This can actually work pretty well, because calls pointers are usually bigger than small common integers, and usually have to be aligned. But yes, this can cause collection of some objects to be delayed. The Boehm collector for C works this way, because it's library-based and so don't get any specific help from the compiler.

There are also GCs that are more tightly coupled to the language they're used in, and actually know the structure of the objects in memory. I've never read up specifically in stack frame handling, but you could record information to help the GC if the compiler and GC are designed to work together. One trick would be putting all the pointer references together and using one word per stack frame to record how many there are, which is not such a huge overhead. If you can work out what function corresponds to each stack frame without adding a word saying so, then you could have a per-function "stack frame layout map" compiled in. Another option would be to use tagged words, where you set the low order bit of words that are not pointers to 1, which (due to address alignment) is never needed for pointers, so you can tell them apart. That means you have to shift unboxed values in order to use them though.

答案4

得分: 8

重要的是要意识到,GHC维护着自己的堆栈,并且不使用C堆栈(除了用于FFI调用)。没有一种可移植的方式可以访问C堆栈的所有内容(例如,在SPARC中,其中一部分内容被隐藏在寄存器窗口中),因此GHC维护了一个堆栈,它可以完全控制。一旦你维护了自己的堆栈,你可以选择任何方案来区分堆栈上的指针和非指针(比如使用位图)。

英文:

It's important to realize that GHC maintains its own stack and does not use the C stack (other than for FFI calls). There's no portable way to access all of the contents of the C stack (for instance, in a SPARC some of it is hidden away in register windows), so GHC maintains a stack where it has full control. Once you maintain your own stack you can pick any scheme to distinguish pointers from non-pointers on the stack (like a using a bitmap).

huangapple
  • 本文由 发表于 2012年5月23日 05:41:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/10710603.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定