集合

最终,您将希望在程序中使用动态数据结构(也称为集合)。std 提供了一组常见的集合:VecStringHashMap 等。std 中实现的所有集合都使用全局动态内存分配器(也称为堆)。

由于 core 本质上不包含内存分配,因此这些实现不可用,但可以在编译器附带的 alloc crate 中找到。

如果您需要集合,堆分配实现并不是您的唯一选择。您还可以使用固定容量集合;可以在 heapless crate 中找到一个这样的实现。

在本节中,我们将探讨和比较这两种实现。

使用 alloc

alloc crate 与标准 Rust 发行版一起提供。要导入 crate,您可以直接 use 它,无需 在您的 Cargo.toml 文件中将其声明为依赖项。

#![feature(alloc)]

extern crate alloc;

use alloc::vec::Vec;

要能够使用任何集合,您首先需要使用 global_allocator 属性来声明程序将使用的全局分配器。您选择的分配器必须实现 GlobalAlloc 特性。

为了完整起见,并使本节尽可能自包含,我们将实现一个简单的碰撞指针分配器,并将其用作全局分配器。但是,我们强烈建议您在程序中使用来自 crates.io 的经过实战检验的分配器,而不是此分配器。

// Bump pointer allocator implementation

use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
use core::ptr;

use cortex_m::interrupt;

// Bump pointer allocator for *single* core systems
struct BumpPointerAlloc {
    head: UnsafeCell<usize>,
    end: usize,
}

unsafe impl Sync for BumpPointerAlloc {}

unsafe impl GlobalAlloc for BumpPointerAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        // `interrupt::free` is a critical section that makes our allocator safe
        // to use from within interrupts
        interrupt::free(|_| {
            let head = self.head.get();
            let size = layout.size();
            let align = layout.align();
            let align_mask = !(align - 1);

            // move start up to the next alignment boundary
            let start = (*head + align - 1) & align_mask;

            if start + size > self.end {
                // a null pointer signal an Out Of Memory condition
                ptr::null_mut()
            } else {
                *head = start + size;
                start as *mut u8
            }
        })
    }

    unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
        // this allocator never deallocates memory
    }
}

// Declaration of the global memory allocator
// NOTE the user must ensure that the memory region `[0x2000_0100, 0x2000_0200]`
// is not used by other parts of the program
#[global_allocator]
static HEAP: BumpPointerAlloc = BumpPointerAlloc {
    head: UnsafeCell::new(0x2000_0100),
    end: 0x2000_0200,
};

除了选择全局分配器之外,用户还必须使用不稳定alloc_error_handler 属性定义如何处理内存不足 (OOM) 错误。

#![feature(alloc_error_handler)]

use cortex_m::asm;

#[alloc_error_handler]
fn on_oom(_layout: Layout) -> ! {
    asm::bkpt();

    loop {}
}

一旦所有这些都到位,用户就可以最终使用 alloc 中的集合。

#[entry]
fn main() -> ! {
    let mut xs = Vec::new();

    xs.push(42);
    assert!(xs.pop(), Some(42));

    loop {
        // ..
    }
}

如果您使用过 std crate 中的集合,那么这些集合将很熟悉,因为它们是完全相同的实现。

使用 heapless

heapless 不需要任何设置,因为它的集合不依赖于全局内存分配器。只需 use 它的集合,然后继续实例化它们。

// heapless version: v0.4.x
use heapless::Vec;
use heapless::consts::*;

#[entry]
fn main() -> ! {
    let mut xs: Vec<_, U8> = Vec::new();

    xs.push(42).unwrap();
    assert_eq!(xs.pop(), Some(42));
    loop {}
}

您会注意到这些集合与 alloc 中的集合之间有两个区别。

首先,您必须预先声明集合的容量。heapless 集合永远不会重新分配,并且具有固定容量;此容量是集合类型签名的组成部分。在本例中,我们已声明 xs 的容量为 8 个元素,即向量最多可以容纳 8 个元素。这由类型签名中的 U8(参见 typenum)指示。

其次,push 方法以及许多其他方法都返回一个 Result。由于 heapless 集合具有固定容量,因此所有将元素插入集合的操作都可能失败。API 通过返回一个 Result 来反映此问题,指示操作是否成功。相比之下,alloc 集合将在堆上重新分配自身以增加其容量。

从 v0.4.x 版本开始,所有 heapless 集合都将所有元素内联存储。这意味着像 let x = heapless::Vec::new(); 这样的操作将在堆栈上分配集合,但也可以在 static 变量上甚至堆上(Box<Vec<_, _>>)分配集合。

权衡

在选择堆分配的可重定位集合和固定容量集合之间时,请牢记这些因素。

内存不足和错误处理

使用堆分配,内存不足始终是一种可能性,并且可能发生在任何集合可能需要增长的地方:例如,所有 alloc::Vec.push 调用都可能产生内存不足条件。因此,某些操作可能隐式失败。一些 alloc 集合公开了 try_reserve 方法,这些方法允许您在增长集合时检查潜在的内存不足条件,但您需要主动使用它们。

如果您专门使用 heapless 集合,并且您没有为其他任何内容使用内存分配器,那么内存不足条件是不可能的。相反,您将不得不逐案处理集合容量不足的情况。也就是说,您将不得不处理 heapless::Vec.push 等方法返回的所有 Result

内存不足故障可能比在 heapless::Vec.push 返回的所有 Result 上进行 unwrap 更难调试,因为观察到的故障位置可能与问题原因的位置匹配。例如,即使 vec.reserve(1) 也可能触发内存不足,如果分配器几乎耗尽,因为其他一些集合正在泄漏内存(内存泄漏在安全 Rust 中是可能的)。

内存使用情况

推断堆分配集合的内存使用情况很困难,因为长期存在的集合的容量可以在运行时发生变化。某些操作可能会隐式地重新分配集合,从而增加其内存使用量,而某些集合公开了 shrink_to_fit 等方法,这些方法可能会减少集合使用的内存——最终,分配器将决定是否实际缩小内存分配。此外,分配器可能必须处理内存碎片,这会增加明显的内存使用量。

另一方面,如果您专门使用固定容量集合,将大多数集合存储在 static 变量中,并为调用堆栈设置最大大小,那么链接器将检测到您是否尝试使用超过物理可用内存的内存。

此外,在堆栈上分配的固定容量集合将由 -Z emit-stack-sizes 标志报告,这意味着分析堆栈使用情况的工具(如 stack-sizes)将将其包含在分析中。

但是,固定容量集合不能缩小,这会导致比可重定位集合所能达到的更低的负载因子(集合大小与其容量之比)。

最坏情况执行时间 (WCET)

如果您正在构建时间敏感型应用程序或硬实时应用程序,那么您可能非常关心程序不同部分的最坏情况执行时间。

alloc 集合可以重新分配,因此可能增长集合的操作的 WCET 也将包括重新分配集合所需的时间,而这本身取决于集合的运行时容量。这使得难以确定例如 alloc::Vec.push 操作的 WCET,因为它取决于所使用的分配器及其运行时容量。

另一方面,固定容量集合永远不会重新分配,因此所有操作都具有可预测的执行时间。例如,heapless::Vec.push 在恒定时间内执行。

易用性

alloc 需要设置全局分配器,而 heapless 则不需要。但是,heapless 需要您选择要实例化的每个集合的容量。

alloc API 将为几乎所有 Rust 开发人员所熟悉。heapless API 试图密切模仿 alloc API,但由于其显式错误处理,它永远不会完全相同——一些开发人员可能会觉得显式错误处理过分或过于繁琐。