为共享数组子范围设计的智能垃圾收集器?

12
这个关于为什么C#中子字符串需要O(n)时间复杂度的流行问题中,一个主要提供的答案认为,如果分配了一个大数组并且通过让新字符串只引用小数组片段来计算子字符串,则即使原始字符串不再被引用,垃圾收集器也无法回收包含较大字符串的字符数组。
这似乎是一个完全有效的答案,但从理论上讲,似乎可以构建一个允许大部分数组被垃圾收集的数组垃圾收集器,同时留下一些仍在使用的小子数组。换句话说,如果有一个50,000元素的数组,其中只有一个小的100元素切片仍在使用,垃圾收集器可以将数组分成三个部分- 100元素切片之前的元素,100元素切片本身和100元素切片之后的元素- 然后回收这些部分的第一个和最后一个。
我的问题是是否有任何语言实现实际使用这种类型的垃圾收集器,或者它只存在于理论上。是否有人知道具有此类垃圾收集器的语言实现的示例?

1
GC与对象内部的摆弄似乎过于复杂 - 抽象是王道。让对象告诉GC它们共享多少内存以及如何共享,并提供回调函数来通过复制另一个数据来删除共享,这种方法听起来更实用一些...尽管我认为它不会有太大回报。正如你所说,这对应的子字符串很小(因此O(n)并没有太大意义),而且GC变得如此聪明也会带来一些性能和存储开销。 - user395760
5个回答

6
理论上,是可以的。但是垃圾回收器存在一个问题:为了回收垃圾,它需要知道存储在内存中的数据布局,并且还必须存储数据以指示内存块是否正在使用...但是布局信息与运行时共享,因为运行时需要知道对象类型(即内存布局)以进行类型转换。
垃圾回收器的工作原理是什么?
垃圾回收器开始读取它所知道的根对象。它获取所有引用并将它们标记为“正在使用”。对于这些被引用的对象,它获取布局并从这些对象中读取更多引用,并将它们标记为“正在使用”...这个过程一直持续,直到没有更��的引用为止。
注:我使用了类型信息和布局信息,意思相同。
例子:
Imagine we have some object layouts:
====================================
A: { int, object, double, object }
B: { object, object }
C: { int }


Memory Data:
============
Now we need to describe what is in memory.
The following list is composed of memory positions,
and the data contained in each position.
There are 2 kinds of notation I will use:
  - Address: ROOT[ list-of-root-references ]
  - Address: type-identifier { object-data }
     Note that each object can span multiple memory
     positions from the first one.
     e.g. 90: B { 0, 0 } -- this reads as
       "stored at address 90: type-identifier of B followed by raw data { 0, 0 }"
  - A reference is represented by a number,
    that point to the memory address of the pointed object.

 1: ROOT[ 90, 20, 30 ]
20: A { 1236, 30, 0.5, 60 }
30: B { 60, 70 }
40: C { 1237 }
50: A { 1238, 20, 0.8, 50 }    There is a self-reference here!
60: C { 1234 }
70: A { 1234, 0, 0.7, 80 }
80: C { 1235 }
90: B { 0, 0 }                 Note that 0 is a null reference!

   The GC need to know the layout of each object.
   Otherwise it wouldn't be abled to tell
   what knid of information is stored in each memory position.

Running the GC:
===============
Garbage collecting steps, to clean the memory described above:
Step 1: Get ROOT references: 2, 3, 9 and mark as 'in-use'
Step 2: Get references from 2, 3 and 9: 3, 6, 7. Note that 0 is a null reference!
Step 3: Get references from 3, 6 and 7: 6, 7, 8, and mark them.
Step 4: Get references from 6, 7, 8, and mark them: only 8!
Step 5: Get references from 8... it has none! We are finished with marking objects.
Step 6: Delete unmarked objects.


      This shows what happened in each step with each object stored in the memory.

Step ->  1  2  3  4  5
Memory                    
20       x           
30       x  x        
40                   DELETE
50                   DELETE
60          x  x     
70          x  x     
80             x  x  
90       x           

我所描述的是一个非常基础的垃圾回收算法。 看一下三色标记......那真是太棒了! 这就是现代真正的垃圾回收是如何实现的。 垃圾回收(计算机科学) - 描述了一些现代GC的方法。
但是...布局信息在哪里存储呢? 这个问题很重要,因为它影响到GC和运行时两方面。 为了进行快速类型转换,类型信息必须放置在引用附近或分配的内存附近。 我们可以考虑在已分配内存块的目录中存储类型信息, 但是......类型转换需要访问目录, 就像new运算符和GC在需要删除对象时一样。 如果将布局信息存储在引用附近,则对同一对象的每个引用都会重复相同的信息, 以及指针本身。
例如:
To represent the memory data I will introduce the following notation:
 - Address: { object-data } -- this time object type is not at the begining!
 - A reference is represented by a type-identifier and an address-number,
   that point to the memory address of the pointed object,
   in the following format: type/number...
   e.g. A/20 -- this reads as: "at address 20, there is an object of type A"
Note that: 0/0 is a null reference,
      but it still requires space to store the type.

The memory would look like this:
 1: ROOT[ B/90, A/20, B/30 ]
20: { 1236, B/30, 0.5, C/60 }
30: { C/60, A/70 }
40: { 1237 }
50: { 1238, A/20, 0.8, A/50 }
60: { 1234 }
70: { 1234, 0/0, 0.7, C/80 }
80: { 1235 }
90: { 0/0, 0/0 }

如果我们将布局信息存储在已分配的内存块附近,那就很好!这样做很快,并且避免了重复的布局信息。
示例:
The memory looks like the first sample:
 *This is the same notation used at the begining of this answer.
 1: ROOT[ 90, 20, 30 ]
20: A { 1236, 30, 0.5, 60 }
30: B { 60, 70 }
40: C { 1237 }
50: A { 1238, 20, 0.8, 50 }
60: C { 1234 }
70: A { 1234, 0, 0.7, 80 }
80: C { 1235 }
90: B { 0, 0 }

到目前为止,一切都很好...但现在我想要共享内存。

我们首先注意到的是,我们不能再将布局信息存储在已分配的内存附近。

想象一下一个带有共享内存的数组:

示例:

 I'll introduce a new notation for arrays:
    type-identifier < array-length >

 1: ROOT[ 20, 31 ] -- pointer 31 is invalid, destination has no type-identifier.
20: INT<5>  -- info about layout of the next memory data (spans by 10 memory units)
30:  2
31:  3 -- should have type-identifier, because someone
32:  5              is pointing here!! Invalid memory!!
33:  7
34: 11

我们仍然可以尝试将布局信息放置在指针旁边。现在,共享内存数组是可能的:
示例:
 1: ROOT[ INT<5>/30, INT<2>/31 ]
30:  2
31:  3
32:  5
33:  7
34: 11

记住,这种方法会使我们在每个地方重复布局信息...但关键是要使用更少的内存,不是吗?但是要共享内存,我们需要更多的内存来存储布局数据/指针。我们还没有甜甜圈。 =(
只有一个希望:让我们降级运行时功能!
这是我的答案-我认为可能是这样做的=> 使用内存分配目录来存储类型信息怎么样?
这可以做到,但是动态转换和GC本身也会受到影响。记住我说过GC需要访问内存目录才能删除对象...现在它需要每次找到引用时都访问目录,而不仅仅是删除。天啊!!我们即将用此杀死GC性能,也会影响运行时性能。我认为代价太高了! <= 这是我的答案
但是...如果运行时不支持动态转换呢?如果编译器在编译时就知道有关类型的所有信息...那么GC甚至不存在...它需要有关类型的信息,因为这些信息告诉它该类型使用的内存的布局是什么。

没有简单、聪明的解决方案在视野中。

也许我完全错了。但是我无法想象有比现在更好的方法。 现代GC甚至比这个更复杂... 我在这里只涵盖了基础知识, 我认为,现代GC正在通过其他更可靠的方式进行优化。

其他参考资料:

http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

http://www.memorymanagement.org/glossary/t.html

http://www.cs.purdue.edu/homes/sbarakat/cs456/gc-2.pdf

三色增量更新GC:是否需要扫描两次每个堆栈?


3

这看起来很像我正在寻找的东西!我怀疑 D 会做类似的事情,但我找不到任何硬文档支持这种说法。 - templatetypedef
1
额,这实际上回答了@templatetypedef的问题吗?链接明确表示“垃圾收集器负责清理不再被任何切片引用的动态数组。”--> 显然,清理是一个全有或全无的操作,对吧?在我看来,它看起来恰好不是我们正在讨论的行为。 - user541686
1
@Mehrdad- 在重新阅读这个链接后,我必须同意你的看法。我曾经理解这个讨论是指有一个很酷的分配器可以释放数组的部分,但实际上更准确地说它只是足够聪明,知道要分配或释放哪些块作为整体。 - templatetypedef
我发现关于D语言动态数组的文章相当令人困惑。我不知道他们是否在说它可以或者不可以像这个问题提议那样进行垃圾回收...如果它能够实现,那么所有它所声称的性能都将变得非常好!=)这篇文章似乎表明它可以,但我不能确定:“当没有切片引用该数据时会发生什么?进入D的垃圾回收器。垃圾回收器负责清理不再被任何切片引用的动态数组。”,但他们并没有告诉我们如何实现这一点。这可能是...也可能不是。如果有人能够澄清这一点,那就太好了。 - Miguel Angelo

1
有没有人知道有没有实现这样垃圾回收器的语言实例?
我认为目前没有任何语言实现这种垃圾回收器。
真正的语言最接近的是什么?函数式语言通常会用分层树替换平面数组。因此,大字符串由称为绳子的数据结构表示。如果程序从绳子中取子串,则可以收集大部分字符串,而无需复制仍然在绳子内可访问的所有数据。但是,这些函数式数据结构比平面数组慢得多。
为什么我们不这样做?可能是因为人们认为这些方法引入了很多复杂性,并解决了一个相对不重要的问题(我从来没有遇到过切片保留太多可访问空间的问题)。此外,使用禁止内部指针的GC的传统,因此GC无法理解切片。特别是,生产GC都将每个对象的元数据放在内存块前面的头部。

我实际上编写了一个虚拟机(HLVM),通过使用四字节引用来避免头文件,即元数据随引用一起传递,而不是在指向的对象中。从引用计数研究中我们知道,绝大多数对象的引用计数为1,因此每个引用头的潜在重复实际上比你想象的要便宜。没有头文件意味着HLVM对数组的内部表示与C兼容,因此互操作性更加容易和快速。同样,切片可以是数组中间的引用。适当的GC算法,例如标记区域,可以释放不再可达的堆分配块的部分并重新使用它们,同时保留可达的切片。

另一个解决方案是使用虚拟内存。您可以将逻辑页面映射到相同的物理页面上,GC可以收集不可达页面。然而,这是非常粗粒度的,因此更加专业化。


0

当然,制作一个“更智能”的垃圾收集器的危险在于,你可能会以某种方式削弱用户,要么是阻止代码工作,要么是为了过度热衷的垃圾收集器而使用笨拙的解决方法。


我不明白这如何回答问题。你能详细说明一下这会如何破坏现有的代码吗? - templatetypedef
我认为这只是一种理论,我想不出任何现有的语言。但是回答您在上面评论中的问题:这并不常见,但我听说过这是一个值得考虑的问题,特别是在实现垃圾收集时引入新功能时。这里有一些结果- http://goo.gl/rg4If - Paul C

0

我认为所有关联数组的实现(PERL、PHP、javascript等)都必须这样做。你称之为“垃圾回收”,但这意味着特定的项目必须首先被取消设置(删除),以便垃圾收集器知道它们未使用。因此,这是正常的删除/移除/取消设置,这肯定不仅反映在关联数组中,而且还反映在特定语言实现所使用的数据结构中。否则,内存可能会被几乎空的数组耗尽...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接