Rust编程语言的自动内存管理是否需要回收碎片化内存?如果是这样,它是如何实现的?
我的理解是,它的类型系统(所有权类型、借用、Rc
、Arc
)允许在编译时确定何时可以释放一块已分配的内存。
但是,难道不可能出现内存块按一定顺序分配,但按不同顺序释放导致碎片化吗?如果要防止这种情况发生,应该如何做?如果发生了,如何高效地管理这些内存碎片?如果进行了碎片整理,使用的方法论是什么?
Rust编程语言的自动内存管理是否需要回收碎片化内存?如果是这样,它是如何实现的?
我的理解是,它的类型系统(所有权类型、借用、Rc
、Arc
)允许在编译时确定何时可以释放一块已分配的内存。
但是,难道不可能出现内存块按一定顺序分配,但按不同顺序释放导致碎片化吗?如果要防止这种情况发生,应该如何做?如果发生了,如何高效地管理这些内存碎片?如果进行了碎片整理,使用的方法论是什么?
+---+---+---+---+---+---+---+---+
| a | b | c | d | e | f | g | h |
+---+---+---+---+---+---+---+---+
有512字节的空间可用于分配,这些空间通过a
到 g
标识,而最新的空间 h
则保留用于元数据(空闲空间、同一类别中下一页/上一页等)。请注意,类别尺寸越大,最后一个空间浪费的空间就越多,因此较大的分配使用不同的方案。
在释放时,页面保留在类别大小中,直到最后一个空间被释放,此时该页面再次为空白并可以用于另一组。
这对内存消耗意味着什么?
小块分配计划的最大内存消耗是操作系统页面数量,可以计算为每个桶大小所消耗的最大操作系统页面数之和,这本身是该大小的最大并发分配数乘以适合页面的分配数(向上取整)。
这是因为,如果您分配给定大小的 1000 个空间,以散乱的方式释放其中大部分空间,从而在操作系统页面中产生了空洞,然后重新分配相同大小的空间,直到再次达到 1000 ... 那么您的内存消耗是恒定的,因为分配器将使用已经部分填充的操作系统页面中的空闲空间来满足第二波分配。
这意味着小类大小的分配既快速,又不会对碎片化做出太大贡献。
当然,这忽略了一个程序的情况,该程序会进行 100 万个 1 字节分配,以一种方式释放其中大部分,从而使所有页面都被使用,然后再使用相同的方式进行 2 字节、3 字节等分配... 但这似乎是一种病态情况。
1 是的,我在说谎。您还需要考虑分配器内部结构的开销以及它可能缓存一些未使用的操作系统页面以准备将来的分配等因素, ... 但仍足以解释碎片化的影响。
那么,碎片化是否是问题?
嗯,仍然可能是问题。地址空间仍然可能发生碎片化,尽管粒度为操作系统页面。
使用虚拟内存,RAM 不需要连续,只要有足够的空间就可以使用页面。也就是说,地址空间使用户产生了连续内存的错觉,即使内存实际上分散在 RAM 中。
这就是问题所在:这种连续内存的错觉需要找到一个地址空间的连续区域,而这受到碎片化的影响。
这种碎片化并不会在小请求中显示出来,但对于超过页面大小的请求可能会成为一个问题。如今,使用64位指针,这在实践中已经不是什么大问题了(即使只使用47位用户空间),但在32位程序中,它更有可能浮出水面:例如,在32位地址空间中,非常难以mmap一个2GB文件,因为它立即就占用了其中一半空间......假设没有杂散的分配阻止它(在这种情况下,请求将失败)。
战斗是否已经失败?
嗯,系统编程的主要优势是......你可以说系统语言。
如果您的程序具有典型分配器无法处理的分配行为,您可以:
在过去的10年中,我个人从未需要这样做,只是在闲暇时间写分配器玩而已;但这是可能的。
总结一下Matthieu的详细解释--
Rust、C和C++在使用标准内存管理时,确实会导致内存碎片。它们不会进行碎片整理。
但在绝大多数实际应用场景中,内存碎片化非常小,不是问题。
如果出现问题,可以自己编写分配器。
Rust不会碎片整理。我认为谷歌在这个领域做得很好。tcmalloc对于C/C++是多线程友好的,并处理碎片问题,而Golang也从中受益。