15

似乎有一种观点认为,在 64 位架构上没有必要使用“拆分堆栈”运行时模型。我说好像是,因为我还没有看到有人真的这么说,只是围着它跳舞:

典型的多线程程序的内存使用量可以显着减少,因为每个线程不需要最坏情况下的堆栈大小。可以在 32 位地址空间中运行数百万个线程(完整的 NPTL 线程或协同程序)。——伊恩·兰斯·泰勒

...暗示 64 位地址空间已经可以处理它。

和...

... 拆分堆栈的持续开销和狭窄的用例(在 32 位架构上产生大量 I/O 绑定任务)是不可接受的... -- bstrie

两个问题:这就是他们所说的吗?其次,如果是这样,为什么它们在 64 位架构上是不必要的?

4

3 回答 3

19

是的,这就是他们所说的。

拆分堆栈(目前)在 64 位架构上是不必要的,因为 64 位虚拟地址空间非常大,可以包含数百万个堆栈地址范围,如果需要,每个堆栈地址范围都与整个 32 位地址空间一样大。

在当今使用的平面内存模型中,从虚拟地址到物理内存位置的转换是在硬件 MMU的支持下完成的。在amd64上,将 64 位虚拟地址空间的大块保留给您正在创建的每个新堆栈,而仅将第一页 (4kB) 映射到实际 RAM 会更好(这意味着总体上更快)。这样,堆栈将能够根据需要在连续的虚拟地址上增长和收缩(这意味着每个函数序言中的代码更少,这是一个很大的优化),而操作系统重新配置 MMU 以将虚拟地址的每一页映射到实际地址RAM 的空闲页面,每当堆栈增长或缩小高于/低于某些可配置的阈值时。

通过巧妙地选择阈值(例如参见动态数组理论),您可以在平均堆栈操作上实现 O(1) 复杂度,同时保留数百万个堆栈的好处,这些堆栈可以根据需要增长并且只消耗内存他们使用。

PS:当前的 Go 实现远远落后于任何一个 :-)

于 2013-10-18T14:11:39.857 回答
9

Go 核心团队目前正在讨论在未来的 Go 版本中使用连续堆栈的可能性。

拆分堆栈方法很有用,因为堆栈可以更灵活地增长,但它也需要运行时分配相对较大的内存块来分布这些堆栈。关于 Go 的内存使用存在很多混淆,部分原因是这个。

制作连续但可增长(可重定位)的堆栈是一个选项,它可以提供相同的灵活性,并且可能会减少对 Go 内存使用的混淆。以及修复低内存机器上的一些不良极端情况(参见链接线程)。

至于 32 位与 64 位架构的优点/缺点,我认为没有任何直接与使用分段堆栈直接相关。

于 2013-10-18T13:43:01.633 回答
2

更新 Go 1.4(2014 年第四季度)

更改为运行时

在 Go 1.4 之前,运行时(垃圾收集器、并发支持、接口管理、映射、切片、字符串等)大部分是用 C 编写的,并带有一些汇编程序支持。
在 1.4 中,大部分代码已被转换为 Go,以便垃圾收集器可以在运行时扫描程序堆栈并获得有关哪些变量处于活动状态的准确信息

这种重写允许 1.4 中的垃圾收集器完全精确,这意味着它知道程序中所有活动指针的位置。这意味着堆会更小,因为不会有误报使非指针保持活动状态。其他相关更改也减少了堆大小,相对于之前的版本,总体上减小了 10%-30%。

结果是堆栈不再分段,消除了“热拆分”问题。当达到堆栈限制时,分配一个新的更大的堆栈,goroutine 的所有活动帧都被复制到那里,并且任何指向堆栈的指针都会被更新。


初步答案(2014 年 3 月)

Agis Anastasopoulo的文章“ Go 中的连续堆栈”也解决了这个问题

在堆栈边界恰好落在紧密循环中的这种情况下,重复创建和销毁段的开销变得很大。
这被称为 Go 社区内部的“热分裂”问题。

“热拆分”将在 Go 1.3 中通过实现连续堆栈来解决。

现在,当堆栈需要增长时,会发生以下情况,而不是分配新段:

  1. 创建一个更大的新堆栈
  2. 将旧堆栈的内容复制到新堆栈
  3. 重新调整每个复制的指针以指向新地址
  4. 销毁旧堆栈

下面提到一个主要出现在 32 位架构中的问题:

不过有一定的挑战。
1.2 运行时不知道堆栈中指针大小的字是否是实际指针。可能有浮点数和最罕见的整数,如果被解释为指针,实际上会指向数据。

由于缺乏此类知识,垃圾收集器必须保守地将堆栈帧中的所有位置视为根。这留下了内存泄漏的可能性,尤其是在 32 位架构上,因为它们的地址池要小得多

但是,在复制堆栈时,必须避免这种情况,并且在重新调整时只应考虑真正的指针。

虽然工作已经完成关于实时堆栈指针的信息现在嵌入到二进制文件中,并且可供运行时使用。
这意味着不仅 1.3 中的收集器可以精确地堆栈数据,而且现在可以重新调整堆栈指针。

于 2014-03-27T10:21:24.017 回答