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

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

但是,内存块不可能按一个顺序分配,然后按不同的顺序释放,从而导致碎片化吗?如果这是可以预防的,如何预防?如果发生这种情况,如何有效地管理内存片段?如果它们被碎片整理,使用的方法是什么?

推荐答案

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,所以它的内存可能会根据使用而被碎片化,cannot没有程序会被碎片化,因为恼人的块是不可能的.


然而,在我们开始害怕分裂之前,我们必须首先考虑它的含义.

What is the effect of fragmentation?

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

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

的确,值得注意的是,碎片并不是废物的唯一来源;过度分配也是一个常见的"问题":

  • Vec将分配2次方的元素数,但可能只使用2^N + 1个,浪费2^N - 1个插槽
  • BTreeMapHashMap分配的空间比实际使用的多
  • 甚至内存分配器通常也会分配预定义大小的块,因此请求157字节实际上可能会被舍入到196字节,浪费39字节

这还不算内存管理中的浪费,因为内存分配器维护一些状态,以了解它有哪些页面,以及其中使用了什么.

这突显了一个非常重要的事实:如果由于分配方案带来的簿记/开销而消耗了大量内存,那么消除碎片化几乎没有意义,因为您会遇到同样的问题.


How do modern allocators manage memory?

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

典型的分配方案相对简单,但对于较小的请求,它非常擅长降低碎片:

  1. 用于"大"请求的大块内存(接近或超过操作系统页面大小:一般为4kB)
  2. 用于"较小"请求的小块内存

对于小型楼板,根据尺寸定义了许多类别.例如:(0, 8](8, 12](12, 16]...,(164, 196], ..., (..., 512].每个类大小管理自己的操作系统页面列表,并雕刻每个操作系统页面供自己私有使用.4kB操作系统页面上512字节类的一个例子是:

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

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

解除分配时,页面将保持在类大小,直到最后一个插槽解除分配,此时页面再次为空并可用于另一个组.


What does it mean for memory consumption?

small Slab scheme1的最大内存消耗是操作系统页面的数量,可以计算 for each 存储桶大小消耗的操作系统页面的最大数量之和,它本身就是这个大小的最大并发分配数乘以适合页面的分配数(并向上取整).

这是因为如果你分配1000个给定大小的插槽,以一种随意的方式释放它们中的大部分,在操作系统页面上戳洞,然后重新分配相同大小的插槽,直到你再次达到1000个...然后,内存消耗是恒定的,因为分配器将使用已经部分填满的OS页面中的空闲插槽来完成第二波分配.

这意味着小班学生的分配速度很快,但并不会对碎片化造成太大影响.

当然,这忽略了一个程序的情况,该程序将分配1M 1字节,以一种让所有页面都被使用的方式取消分配其中的大部分,然后对2字节、3字节等执行相同的操作...但这似乎是一个病理病例.

1 Yes, I am lying through my teeth. You also need to account for the allocator's internal structures overhead and the fact that it may cache a few unused OS pages to prepare for future allocations, ... still, it's sufficient for explaining the effect of fragmentation.


So, is fragmentation an issue?

好吧,还是有可能的.地址空间仍然可以被分割,尽管在操作系统页面的粒度上.

有了虚拟内存,RAM就不需要是连续的,只要有足够的空间就可以使用这些页面.也就是说,地址空间给了用户连续内存的错觉,即使内存在物理上分布在整个RAM中.

问题就在这里:这种连续内存的错觉需要找到地址空间的一个连续区域,这个区域会被分割.

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


Is the fight lost?

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

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

  • 控制导致问题的特定分配(自己使用sbrk/mmap来处理)
  • 或者只是重写一个经过特别调整的分配器

10年来,我个人从来都不需要这样做,只是在业余时间写分配程序来消遣;但这是可能的.

Rust相关问答推荐

捕获Rust因C++异常而产生panic

Rust:跨多个线程使用hashmap Arc和rwlock

当两者都有效时,为什么Rust编译器建议添加';&;而不是';*';?

如何在Tauri中将变量从后端传递到前端

定义采用更高级类型泛型的性状

铁 rust 中的共享对象实现特征

Rust函数的返回值不能引用局部变量或临时变量

在运行特定测试时,如何 suppress cargo test 的空输出?

是否可以使用Serde/Rust全局处理无效的JSON值?

为什么在 Allocator API 中 allocate() 使用 `[u8]` 而 deallocate 使用 `u8` ?

Rust 中的静态引用

为什么 js_sys Promise::new 需要 FnMut?

可以在旋转循环中调用try_recv()吗?

try 从标准输入获取用户名和密码并删除 \r\n

如何使用 Bincode 在 Rust 中序列化 Enum,同时保留 Enum 判别式而不是索引?

具有在宏扩展中指定的生命周期的枚举变体数据类型

如何获取函数中borrow 的切片的第一部分?

如何在没有 `make_contiguous()` 的情况下对 VecDeque 进行排序或反转?

基于名称是否存在的条件编译

基于名称是否存在的条件编译