#stack-memory #stack #safe #byte-buffer #alloca #stack-allocation

no-std stackalloc

在运行时安全地分配和操作任意大小的切片

7个稳定版本

1.2.1 2024年5月8日
1.2.0 2022年6月15日
1.1.2 2022年4月28日
1.1.1 2021年6月27日
1.0.1 2021年3月27日

#98 in 内存管理

Download history 5/week @ 2024-04-29 129/week @ 2024-05-06 10/week @ 2024-05-13 19/week @ 2024-05-20 8/week @ 2024-05-27 4/week @ 2024-06-03 10/week @ 2024-06-10 8/week @ 2024-06-17 10/week @ 2024-06-24 24/week @ 2024-07-01 4/week @ 2024-07-15 31/week @ 2024-07-29 10/week @ 2024-08-05 25/week @ 2024-08-12

66 每月下载量
用于 2 crates

MIT 许可证

37KB
559

安全运行时栈分配

提供方法,以便Rust可以安全地访问和使用运行时栈分配的缓冲区(如C VLAs或alloca()函数)。这是通过一个辅助函数完成的,该函数接受一个FnOnce闭包,该闭包以栈分配的缓冲区切片为参数。切片仅在闭包返回时被认为是有效的,此时栈将恢复到辅助函数的调用者。如果您需要一个可移动的缓冲区,请使用Vec或静态大小的数组。内存是在闭包调用者的栈帧上分配的,当调用者返回时进行释放。

此切片将根据安全Rust对切片的期望进行适当的构造。但是,仍然可能通过分配过多内存而导致栈溢出,因此请谨慎使用,并且永远不要盲目地分配未检查的栈内存。

要求

该crate适用于稳定或夜间Rust,但需要符合C99规范的编译器来构建。

功能

  • no_std - 在夜间工具链上启用no_std支持。

示例

在栈上分配字节数组。

fn copy_with_buffer<R: Read, W: Write>(mut from: R, mut to: W, bufsize: usize) -> io::Result<usize>
{
  alloca_zeroed(bufsize, move |buf| -> io::Result<usize> {
   let mut read;
   let mut completed = 0;
   while { read = from.read(&mut buf[..])?; read != 0} {
    to.write_all(&buf[..read])?;
    completed += read;
   }
   Ok(completed)
  })
}

任意类型

在栈上分配任何类型的切片。

stackalloc(5, "str", |slice: &mut [&str]| {
 assert_eq!(&slice[..], &["str"; 5]);
});

释放

包装器处理需要释放的类型。

stackalloc_with(5, || vec![String::from("string"); 10], |slice| {
 assert_eq!(&slice[0][0][..], "string");  
}); // The slice's elements will be dropped here

MaybeUninit

您可以直接获取对齐的栈内存,无需初始化。

stackalloc_uninit(5, |slice| {
 for s in slice.iter_mut()
 {
   *s = MaybeUninit::new(String::new());
 }
 // SAFETY: We have just initialised all elements of the slice.
 let slice = unsafe { stackalloc::helpers::slice_assume_init_mut(slice) };

 assert_eq!(&slice[..], &vec![String::new(); 5][..]);

 // SAFETY: We have to manually drop the slice in place to ensure its elements are dropped, as `stackalloc_uninit` does not attempt to drop the potentially uninitialised elements.
 unsafe {
   std::ptr::drop_in_place(slice as *mut [String]);
 }
});

它是如何工作的?

由于Rust没有在运行时操作栈的方法,我们使用FFI调用一个函数来操作帧以在该处分配所需的内存。然后,此函数调用回调,传递此栈分配内存的指针。一旦回调返回,FFI函数将处理重置栈指针到其调用者的栈指针。

与直接在调用者的帧上分配内存相比,这在许多方面都有益处。

  • 在所有支持 alloca 的语言中,它都被实现为编译器内建功能。这是因为编译器必须在函数返回的任何位置插入重置栈指针的指令,它还必须跟踪在 alloca 调用之后推入栈中的任何内容的地址。手动执行此操作将阻止编译器管理栈指针的能力,并防止它不仅确定当前栈帧的大小,而且确定其布局。因此,这可能是更有效率的。
  • 在调用者的帧上直接分配内存意味着内存将保留到函数 返回,而不是直到它超出作用域。这可能是导致错误的微妙来源,例如,在循环中调用 alloca() 导致栈溢出的风险。因此,这也更安全,且风险更低。
  • 以这种方式实现 alloc 需要特定平台的内联汇编。这不仅是不稳定的 Rust 功能,而且非常不安全,因为编译器可能会基于栈布局的假设,这些假设随后会被无效化。目前还没有方法来实现这种行为的安全的 alloca,因为这样的实现需要用户非常了解在哪里以及如何使用它。

缺点

这要以额外的函数调用为代价,这个函数调用可能无法内联(即使有 LTO 和其他激进优化)。与堆分配的缓冲区相比,栈分配的缓冲区的权衡在一般情况下可能不值得,但这个额外的间接引用可能会在微观尺度上影响性能。如果您对性能至关重要,那么您应该无论如何都要对这些事物进行基准测试。这可能是在您的用例中堆分配的 Vec 更高效,或者栈分配的切片可能更高效。

性能

对于小型(1k 或更小)元素数组,stackalloc 可以比 Vec 高出约 50% 或更多。这种性能差异随着分配的内存量增长而减小。

test tests::bench::stackalloc_of_uninit_bytes_known   ... bench:           3 ns/iter (+/- 0)
test tests::bench::stackalloc_of_uninit_bytes_unknown ... bench:           3 ns/iter (+/- 0)
test tests::bench::stackalloc_of_zeroed_bytes_known   ... bench:          22 ns/iter (+/- 0)
test tests::bench::stackalloc_of_zeroed_bytes_unknown ... bench:          17 ns/iter (+/- 0)
test tests::bench::vec_of_uninit_bytes_known          ... bench:          13 ns/iter (+/- 0)
test tests::bench::vec_of_uninit_bytes_unknown        ... bench:          55 ns/iter (+/- 0)
test tests::bench::vec_of_zeroed_bytes_known          ... bench:          36 ns/iter (+/- 2)
test tests::bench::vec_of_zeroed_bytes_unknown        ... bench:          37 ns/iter (+/- 0)

许可

MIT 许可证

无运行时依赖

~205KB