为什么 Rust std::alloc 分配的差距大于预期?

Why does Rust std::alloc allocate with larger than expected gaps?

提问人:Niland Schumacher 提问时间:7/18/2023 最后编辑:Niland Schumacher 更新时间:7/18/2023 访问量:127

问:

如果我使用大小为 256 且对齐方式为 1024 的示例布局进行两次分配,我希望第二次分配在第一次分配之后为 1024 的第一个倍数(即 ptr2 - ptr1 == 1024)。相反,我发现两个分配之间存在两倍于 2048 字节大小的间隙。

use std::alloc::alloc;
use std::alloc::Layout;

fn main() {
    unsafe {
        let size = 256;
        let alignment = 1024
        let large_layout = Layout::from_size_align_unchecked(size, alignment);
        let ptr1 = alloc(large_layout) as usize;
        let ptr2 = alloc(large_layout) as usize;

        // I would expect this to print 1024, but it prints 2048...
        println!("Difference1: {}", ptr2 - ptr1);
    }
}

据我了解,对齐使得分配只出现在对齐的倍数处,这似乎是真的,但似乎还有其他事情正在发生。我知道 alloc 还需要一个空格字来表示 alloc 的大小,这在某些情况下可以解释为什么差距可能比预期的要大。但是,在大小 = 256 且对齐方式 = 1024 的情况下,分配之间应该有足够的空间允许它们背靠背分配?以下是我在不同大小和对齐方式的指针间隙之间进行实验的一些结果。我对这些例子感到困惑,似乎没有四舍五入到最接近的对齐方式,而是差距是我预期的两倍。

| size | alignment | gap  |
| ---- | --------- | ---- |
| 4    | 32        | 32   |
| 8    | 32        | 32   |
| 16   | 32        | 64   | ???
| 32   | 32        | 64   | 
| ---- | --------- | ---- |
| 4    | 64        | 64   |
| 8    | 64        | 64   |
| 16   | 64        | 64   |
| 32   | 64        | 128  | ???
| 64   | 64        | 128  | 
| ---- | --------- | ---- |
| 256  | 1024      | 2048 | ???
| 512  | 1024      | 2048 | ???
| 1024 | 1024      | 2048 | 
| ---- | --------- | ---- |
Rust 内存管理 分配

评论

0赞 tadman 7/18/2023
这是一个非常大的对齐值,不是吗?为什么您的大小值始终为 <= 对齐方式?在实践中,它应该是相反的。
0赞 Niland Schumacher 7/18/2023
是的,你可能是对的哈哈,我还在学习很多关于内存管理的知识,这是一个相当小众的问题。但是,我正在尝试为解释器制作一个内存管理器,该解释器一次分配 4kb 的块。当解释器进行解析时,它会运行相当多的 std::alloc 来创建 AST,并帮助生成字节码。然后,它完全切换到自定义分配器以运行程序。如果没有 4kb 的大块对齐,我担心在程序开始时进行的小分配可能会导致碎片,最坏的情况可能是 3.99kb 的孔。
1赞 tadman 7/18/2023
我认为您不寻常的对齐要求可能会导致碎片化。如果你要分配很多相同类型的东西,可以考虑将它们分配到更大的块中,然后使用池进行子分配,甚至只是把这些都塞进一个 .您甚至可能不需要直接点击分配器。Vec

答:

2赞 Matthieu M. 7/18/2023 #1

接口与实现

首先,是一个接口,而不是一个实现。根据您的平台和传递给标准库的标志,您最终可能会得到系统分配器(Windows 和 Unix 之间不同)或 musl 分配器等......std::alloc::alloc

std::alloc::alloc只是一个关于标准库使用的任何内存分配器的薄抽象层,提供统一的接口,但不一定是统一的行为:提供的唯一行为保证是(简化)如果你确实得到了一个指针,它将遵循所需的布局,并且提供的内存片在其整个生命周期内不会与任何其他分配重叠......仅此而已。

尺寸与对齐方式

为了提高效率,内存分配器倾向于使用平板运行,尤其是对于小尺寸。简而言之,他们选择一个内存区域,并将其切成大小相等的块。那是:

  • 最多 8 个字节块的板。
  • 最多 12 个字节块的 B 板。
  • 最多 16 个字节块的 C 板。
  • ...

这意味着,当他们收到需要对齐 2 n 字节的请求时,他们将选择块大小至少为 2n 字节的板,因为这是满足请求的最简单方法。

这实际上在 Layout::from_size_align 中有所暗示

从给定的 和 构造布局,如果不满足以下任一条件,则返回:sizealignLayoutError

  • align不能为零,
  • align必须是 2 的幂,
  • size,当四舍五入到最接近的 的倍数时,不得溢出(即,四舍五入后的值必须小于 或 等于 )。alignisizeisize::MAX

请注意最后一行,谈到四舍五入到最接近的 的倍数。sizealign

不能保证会发生四舍五入,但实现可能希望四舍五入,因此保证 in 确保它可以合理地做到这一点。Layout

小间隙

可以肯定的是,你需要确定你正在使用哪个底层实现——可能是你的系统分配器,这取决于你使用的平台——并且需要有人深入研究源代码。

无论您使用哪个内存分配器,标头都可能在分配之前附加,它存储自己的元数据,这需要“填充”分配的大小,有时会导致将分配“颠簸”到下一个分配类。

或者,您可能会看到一个安全功能在起作用,其中金丝雀被预置/追加以捕获杂散记忆后写。

或。。。

大差距

板坯通常不用于大尺寸。相反,在某些时候,分配器通常会切换到使用整数的操作系统页面。在 x86 上,典型的操作系统页面是 4KB,所以当接近 4KB 时,你会看到一个行为开关:告别细粒度的板,你好粗粒度的页面。

您使用的特定分配器很可能已经开始以 1KB 开始切换;特别是,对于 1KB,它很可能不会尝试将标头放入分配本身(即使您要求较小的大小)。

但是差距!

是的,差距。

与任何软件一样,内存分配器需要权衡取舍。通常,在现代系统上,它们倾向于快速分配/释放,而不是占用大量内存。这意味着它们不会被优化以在尽可能紧凑的空间内容纳尽可能多的分配;无论如何,不会以牺牲速度为代价。

因为,让我们面对现实吧,用户最关心的是速度。