提问人:Niland Schumacher 提问时间:7/18/2023 最后编辑:Niland Schumacher 更新时间:7/18/2023 访问量:127
为什么 Rust std::alloc 分配的差距大于预期?
Why does Rust std::alloc allocate with larger than expected gaps?
问:
如果我使用大小为 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 |
| ---- | --------- | ---- |
答:
接口与实现
首先,是一个接口,而不是一个实现。根据您的平台和传递给标准库的标志,您最终可能会得到系统分配器(Windows 和 Unix 之间不同)或 musl 分配器等......std::alloc::alloc
std::alloc::alloc
只是一个关于标准库使用的任何内存分配器的薄抽象层,提供统一的接口,但不一定是统一的行为:提供的唯一行为保证是(简化)如果你确实得到了一个指针,它将遵循所需的布局,并且提供的内存片在其整个生命周期内不会与任何其他分配重叠......仅此而已。
尺寸与对齐方式
为了提高效率,内存分配器倾向于使用平板运行,尤其是对于小尺寸。简而言之,他们选择一个内存区域,并将其切成大小相等的块。那是:
- 最多 8 个字节块的板。
- 最多 12 个字节块的 B 板。
- 最多 16 个字节块的 C 板。
- ...
这意味着,当他们收到需要对齐 2 n 字节的请求时,他们将选择块大小至少为 2n 字节的板,因为这是满足请求的最简单方法。
这实际上在 Layout::from_size_align 中有所暗示:
从给定的 和 构造布局,如果不满足以下任一条件,则返回:
size
align
LayoutError
align
不能为零,align
必须是 2 的幂,size
,当四舍五入到最接近的 的倍数时,不得溢出(即,四舍五入后的值必须小于 或 等于 )。align
isize
isize::MAX
请注意最后一行,谈到四舍五入到最接近的 的倍数。size
align
不能保证会发生四舍五入,但实现可能希望四舍五入,因此保证 in 确保它可以合理地做到这一点。Layout
小间隙
可以肯定的是,你需要确定你正在使用哪个底层实现——可能是你的系统分配器,这取决于你使用的平台——并且需要有人深入研究源代码。
无论您使用哪个内存分配器,标头都可能在分配之前附加,它存储自己的元数据,这需要“填充”分配的大小,有时会导致将分配“颠簸”到下一个分配类。
或者,您可能会看到一个安全功能在起作用,其中金丝雀被预置/追加以捕获杂散记忆后写。
或。。。
大差距
板坯通常不用于大尺寸。相反,在某些时候,分配器通常会切换到使用整数的操作系统页面。在 x86 上,典型的操作系统页面是 4KB,所以当接近 4KB 时,你会看到一个行为开关:告别细粒度的板,你好粗粒度的页面。
您使用的特定分配器很可能已经开始以 1KB 开始切换;特别是,对于 1KB,它很可能不会尝试将标头放入分配本身(即使您要求较小的大小)。
但是差距!
是的,差距。
与任何软件一样,内存分配器需要权衡取舍。通常,在现代系统上,它们倾向于快速分配/释放,而不是占用大量内存。这意味着它们不会被优化以在尽可能紧凑的空间内容纳尽可能多的分配;无论如何,不会以牺牲速度为代价。
因为,让我们面对现实吧,用户最关心的是速度。
评论
Vec