似乎有一种观点认为在64位体系结构上使用“拆分堆栈”运行时模型是不必要的。我说似乎是,因为我没有看到有人真正说过,只能在它周围跳舞:
典型的多线程程序的内存使用量可能会减少 很明显,因为每个线程不需要最坏情况的堆栈 尺寸。它可以运行数百万个线程(完整的NPTL) 线程或协同例程)在32位地址空间中。 - Ian Lance Taylor
...暗示64位地址空间已经能够处理它。
和...
......拆分堆栈的持续开销和狭窄的用例 (在32位体系结构上产生大量的I / O绑定任务) 是不可接受的...... - bstrie
两个问题:这是他们说的吗?其次,如果是这样,为什么它们在64位架构上不必要?
答案 0 :(得分:19)
是的,这就是他们所说的。
64位架构上(当前)不需要拆分堆栈,因为64位虚拟地址空间非常大,可以包含数百万个堆栈地址范围,如果需要,每个堆栈地址范围都大到整个32位地址空间。
在当前使用的Flat memory model中,在hardware MMU的支持下,从虚拟地址到物理内存位置的转换完成。在amd64上,事实证明,将64位虚拟地址空间的大块保留给正在创建的每个新堆栈更好(意味着整体更快),同时只将第一页(4kB)映射到实际RAM。这样,堆栈将能够根据需要增加和缩小连续的虚拟地址(意味着每个function prologue中的代码更少,这是一个很大的优化),而操作系统重新配置MMU以映射每页虚拟地址每当堆栈增长或收缩高于/低于某些可配置的阈值时,到RAM的实际空闲页面。
通过巧妙地选择阈值(参见例如dynamic arrays的理论),您可以在平均堆栈操作上实现O(1)复杂性,同时保留数百万可以增长的堆栈的好处需要并且只消耗他们使用的内存。
PS:当前的Go实现远远落后于任何一个: - )
答案 1 :(得分:9)
Go核心团队目前discussing可以在未来的Go版本中使用连续堆栈。
拆分堆栈方法很有用,因为堆栈可以更灵活地增长,但它还要求运行时分配相对较大的内存块来分配这些堆栈。关于Go的内存使用情况有很多confusion,部分原因在于此。
制作连续但可增长(可重定位)的堆栈是一种可提供相同灵活性的选项,可以减少对Go内存使用的混淆。除了在低内存机器上修复一些不良案例(参见链接线程)。
至于32位与64位架构的优缺点,我认为没有任何直接关联使用分段堆栈。
答案 2 :(得分:2)
更新Go 1.4(2014年第4季度)
直到Go 1.4,运行时(垃圾收集器,并发支持,接口管理,映射,切片,字符串......)主要是用C编写的,有一些汇编程序支持。
在1.4中,大部分代码已经转换为Go,以便垃圾收集器可以扫描运行时的程序堆栈,并获得有关哪些变量处于活动状态的准确信息。这种重写允许1.4中的垃圾收集器完全精确,这意味着它知道程序中所有活动指针的位置。这意味着堆将更小,因为没有误报使非指针保持活动状态。其他相关更改也会减少堆大小,相对于先前版本,堆大小总体上小10%-30%。
结果是堆栈不再分段,消除了“热分裂”问题。当达到堆栈限制时,会分配一个新的更大的堆栈,goroutine的所有活动帧都会被复制到那里,并且任何指向堆栈的指针都会更新。
初步答复(2014年3月)
Contiguous stacks in Go的文章“Agis Anastasopoulo”也解决了这个问题
在堆栈边界恰好落入紧密循环的情况下,重复创建和销毁段的开销变得很大。
这被称为Go社区内的“热门分裂”问题。通过实施连续堆栈,将在Go 1.3中解决“热拆分”。
现在当堆栈需要增长时,不会分配新的段,而是发生以下情况:
- 创建一个新的,更大的堆栈
- 将旧堆栈的内容复制到新堆栈
- 重新调整每个复制的指针以指向新地址
- 销毁旧堆栈
醇>
以下提到主要在32位架构中看到的一个问题:
虽然有一定的挑战 1.2运行时不知道堆栈中的指针大小的字是否是实际指针。可能有浮点数,而且很少有整数,如果解释为指针,实际上会指向数据。
由于缺乏这样的知识,垃圾收集器必须保守地认为堆栈帧中的所有位置都是根。这留下了内存泄漏的可能性,特别是在32位体系结构上,因为它们的地址池要小得多。
然而,在复制堆栈时,必须避免这种情况,并且在重新调整时只应考虑实际指针。
Work was done though和information about live stack pointers现已嵌入二进制文件中,可供运行时使用 这意味着不仅1.3中的收集器可以 precisely 堆栈数据,而且现在可以重新调整堆栈指针。