首页 文章

Rust的内存管理会导致内存碎片吗?

提问于
浏览
15

Rust编程语言的自动内存管理是否需要回收碎片内存?如果是这样,它是如何做到的?

我的理解是它的类型系统(所有权类型,借用, RcArc )允许它在编译时确定性地知道何时可以释放一大块已分配的内存 .

但是,内存块是否可能在一个订单中分配,并以不同的顺序释放,从而导致碎片?如果这被阻止了,怎么样?如果确实发生了这种情况,那么如何有效地管理内存片段?如果他们进行碎片整理,那么使用的方法是什么?

2 回答

  • 29

    TL;DR: Most programs will never have to worry about fragmentation in C, C++ or Rust. Those which do will have to handle it by themselves.


    Rust编程语言的自动内存管理是否需要回收碎片内存?

    Rust没有自动内存管理功能;它具有手动内存管理,编译器会检查其是否正确 . 差异可能听起来很理论,但这很重要,因为它意味着内存操作直接映射到源代码,幕后没有任何魔力 .

    通常,语言需要具有压缩GC才能压缩碎片存储器 . Rust与C和C一样,没有GC,因此根据使用情况,它的内存可能会碎片化,并且如果没有程序释放恼人的块,则无法进行碎片整理,因为重定位是不可能的 .


    但是,在我们开始担心碎片化之前,我们必须先考虑它的含义 .

    What is the effect of fragmentation?

    碎片会造成物理内存和地址空间的浪费:您的程序占用的空间超过了它的使用量 . 在极端情况下,即使未使用的内存量足以授予它们,这种浪费也可能阻止分配请求 .

    与GC并行时,重要的是要意识到大多数GC语言也会造成一些浪费 .

    实际上,值得注意的是,支离破碎不是唯一的浪费来源;过度分配也是一个常见的“问题”:

    • a Vec 将分配2个幂的元素,但也许你只使用 2^N + 1 ,浪费 2^N - 1 个槽

    • a BTreeMapHashMap 分配的空间比实际使用的空间大

    • 甚至内存分配器通常会分配预定义大小的块,因此要求157个字节实际上可能会被舍入到196个字节,浪费39个字节

    这甚至不计算管理这个内存所带来的浪费,因为内存分配器维护一些状态来知道它有哪些页面,以及它们中使用了什么 .

    这强调了一个非常重要的事实:如果因为你的分配方案规定你会遇到同样的问题,你会消耗这么多内存,那么摆脱碎片是没有意义的 .


    How do modern allocators manage memory?

    现代分配器不是你的da的自由列表分配器 .

    典型的分配方案相对简单,但非常擅长保持小型请求的碎片化:

    • "big"请求的大块内存(关闭或超过操作系统页面大小:一般为4kB)

    • "smaller"请求的小块内存

    对于小板块,许多类由大小定义 . 例如: (0, 8](8, 12](12, 16] ,..., (164, 196] ,..., (..., 512] . 每个类大小管理自己的OS页面列表,并将每个OS页面刻为自己的私有用途 . 4kB OS页面上512字节类的示例可以是:

    +---+---+---+---+---+---+---+---+
    | a | b | c | d | e | f | g | h |
    +---+---+---+---+---+---+---+---+
    

    其中512字节的插槽 ag 可用于分配,最新的插槽 h 保留用于元数据(空闲插槽,同一类中的下一个/上一页等等) . 请注意,类大小越大,在最后一个插槽中浪费的越多,这就是更大的分配使用不同方案的原因 .

    取消分配时,页面保持在类大小,直到最后一个插槽被释放,此时页面再次为空,可以用于另一个组 .

    注意:默认情况下,Rust可执行文件使用jemalloc,此paper具有相关的特定详细信息 .


    What does it mean for memory consumption?

    小平板scheme1的最大内存消耗是一些OS页面,它们可以计算为每个桶大小消耗的OS页面的最大数量之和,它本身是此大小的最大并发分配数乘以除以适合页面的分配数量(并向上舍入) .

    这是因为如果您分配给定大小的1000个插槽,则释放其中大多数是偶然的方式,在操作系统页面中挖空洞,然后重新分配相同大小的插槽,直到再次达到1000 ...然后你的内存消耗是不变的,因为分配器将使用已经存在的空闲插槽部分填充OS页面以完成第二波分配 .

    这意味着小 class 的分配既快又好,但对分片没有多大贡献 .

    当然,这忽略了一个程序的情况,该程序将进行1M 1字节分配,以一种使所有页面都使用的方式释放大部分,然后用2字节,3字节等进行相同的操作......但这看起来像一个病态的案例 .

    1是的,我正在撒谎 . 您还需要考虑分配器's internal structures overhead and the fact that it may cache a few unused OS pages to prepare for future allocations, ... still, it'足以解释碎片的影响 .


    So, is fragmentation an issue?

    好吧,它仍然可以 . 尽管在OS页面的粒度上,地址空间仍然可以分段 .

    对于虚拟内存,RAM不需要是连续的,因此只要有足够的空间就可以使用页面 . 也就是说,即使存储器物理地遍布整个RAM,地址空间也给用户提供了连续存储器的错觉 .

    存在的问题是:这种连续记忆的错觉需要找到一个地址空间的连续区域,这个区域会受到碎片化 .

    这种碎片不会出现在小请求中,但对于超过页面大小的请求,它们可能是个问题 . 目前,使用64位指针,这在实践中的问题要小得多(即使仅使用47位用户用户),但在32位程序中,它更容易浮出水面:例如,它是32位地址空间中的2GB文件非常困难,因为它立即占据了它的一半...假设没有杂散分配阻止它(在这种情况下请求将失败) .


    Is the fight lost?

    那么,系统编程的主要优点是......你可以说系统语言 .

    如果您的程序具有典型分配器无法很好处理的分配行为,您可以:

    • 控制导致问题的特定分配(使用 sbrk / mmap 自己来处理它们)

    • 或只是重写一个专门调整的分配器

    10年来,我个人从未需要,只是在业余时间写作分配器以获得乐趣;但这是可能的 .

  • 2

    总结Matthieu的精彩详细解释 -

    Rust和C和C在使用标准内存管理时会导致内存碎片化 . 他们不进行碎片整理 .

    但是在绝大多数现实世界的用例中,碎片是如此微小,以至于它不是问题 .

    如果这是一个问题,您可以滚动自己的分配器 .

相关问题