#heap-allocation #byte-slice #immutability #byte-array #container #owned #copying

tinybuf

用于多种不可变字节的容器,针对小数组进行了优化

7 个版本

0.3.0 2023年6月22日
0.2.3 2023年4月24日
0.1.1 2023年4月24日

#1037解析器实现


5 个包 (4 直接) 使用

Apache-2.0

30KB
372

TinyBuf

针对不可变所有权的优化容器。存储 Arc<dyn AsRef<[u8]>>Box<[u8]>Box<dyn AsRef<[u8]>>Vec<u8>[u8; N](对于小 N 值),在一个单一类型中(没有泛型)以直接移动的方式进行存储——没有额外的堆分配或复制。

所有 TinyBuf 值都是切片,因此您可以直接获得所有 标准切片方法。TinyBuf 还实现了

  • AsRef<[u8]>
  • Clone
  • Hash
  • PartialEqEq
  • PartialOrdOrd
  • serde::Serializeserde::Deserialize(带有可选的 serde 功能)

目录

用法

将其添加到依赖项中

cargo add tinybuf

在类型位置中使用不可变的 Vec<u8> 替换为 TinyBuf

用例

TinyBuf 适用于以下场景

  • 您想提供或接受不同类型和长度的不可变所有者字节数据。
  • 您想提供或接受长度小于等于 N <= 23 的不可变 [u8; N]

以下是一些演示 TinyBuf 用例的示例。为了说明目的,让我们假设我们正在构建一个内存中的键值存储,并关注值。

接受数据

通常,如果您想接受一些所有者字节数据,您会使用 Vec

struct MyKV {
  entries: HashMap<&'static str, Vec<u8>>,
}

impl MyKV {
  pub fn set(&mut self, key: &'static str, value: Vec<u8>) {
    self.entries.insert(key, value);
  }
}

然而,这对于有数组、boxed slice 等字节的调用者来说并不理想。他们必须重新分配到新的 Vec

let kv: MyKV = MyKV::new();
// Fast, direct move, no reallocation or copying.
kv.set("vec", vec![0, 4, 2]);
// Requires allocating a new Vec and copying into it.
kv.set("boxslice", Box::new(&[0, 4, 2]).to_vec());
// Requires allocating a new Vec and copying into it.
kv.set("array", [0, 4, 2].to_vec());

所以下一个方法可能就是使用泛型

struct MyKV<V: AsRef<[u8]>> {
  entries: HashMap<&'static str, V>,
}

impl<V: AsRef<[u8]>> MyKV<V> {
  pub fn set(&mut self, key: &'static str, value: V) {
    self.entries.insert(key, value);
  }
}

如果值类型始终相同,则此方法有效。但也许您想接受不同类型的字节——也许不同的用户或子系统使用不同的类型,或者有时使用数组来存储小数据以避免堆分配。如果是这样,则上述泛型方法不可行。相反,另一种方法可能是使用boxed特质

struct MyKV {
  entries: HashMap<&'static str, Box<dyn AsRef<[u8]>>>,
}

impl MyKV {
  pub fn get(&self, key: &'static str) -> Option<&Box<dyn AsRef<[u8]>>> {
    self.entries.get(key)
  }

  pub fn set(&mut self, key: &'static str, value: impl AsRef<[u8]>) {
    self.entries.insert(key, Box::new(value));
  }
}

虽然这种方法允许在不复制到 Vec 的情况下存储不同类型的字节,但装箱实际上逆转了这种优化,因为调用 Box::new 总是将它移动到堆上,即使对于 Box<[u8]> 值也是如此。这对于已经位于堆上的数据可能不是太糟糕,因为只有结构体本身而不是数据本身是堆分配的,但它仍然需要分配一个相对较小的堆。

这就是 TinyBuf 的作用所在

struct MyKV {
  entries: HashMap<&'static str, TinyBuf>,
}

impl MyKV {
  pub fn get(&self, key: &'static str) -> Option<&TinyBuf> {
    self.entries.get(key)
  }

  pub fn set<D: Into<TinyBuf>>(&mut self, key: &'static str, value: D) {
    self.entries.insert(key, value);
  }
}

let kv: MyKV = MyKV::new();
// Fast, direct move, stored inline in TinyBuf, no heap allocation or copying of data.
kv.set("boxslice", Box::new(&[0, 4, 2]));
kv.set("static", b"my static value");
kv.set("vec", vec![0, 4, 2]);
// For small arrays: fast, direct move, stored inline in TinyBuf, no heap allocation or copying of data.
kv.set("smallarray", [0, 4, 2]);
// If an array is too big, heap allocation is required, so you must explicitly opt in.
kv.set("largearray", TinyBuf::from_slice(&[0u8; 24]));
// If you already have a Box<dyn AsRef[u8]> or Arc<dyn AsRef<[u8]>> value, TinyBuf will accept it too.
kv.set("boxdyn", my_boxed_asref);
kv.set("arc", my_arced_asref);
// If you have some other type that TinyBuf doesn't support and cannot convert it to one of the supported types, you can created a boxed trait value, which will require heap allocation.
kv.set("custom", Box::<dyn AsRef[u8]>::new(my_asrefable_value));

提供数据

如先前的 MyKV 示例所示,TinyBuf 的一个好处是能够在不进行堆分配或复制的情况下,接受和存储许多不同类型的所有者字节切片。

另一个优点是在从切片读取并希望将其转换为所有者值时,能够动态利用小的内联数组。

fn read_from_vm_memory(&self, start: usize, end: usize) -> Vec<u8> {
  // This will always heap allocate, even if the length is tiny.
  self.mem[start..end].to_vec()
}

fn read_from_vm_memory(&self, start: usize, end: usize) -> TinyBuf {
  // This will only heap allocate if the length is large.
  TinyBuf::from_slice(&self.mem[start..end])
}

请注意,在这两种情况下,由于我们想要拥有数据,所以数据都被复制了,但在第二个 TinyBuf 变体中,如果长度小于等于 23,则不会发生堆分配。这种优化是自动且透明的,不需要额外的条件代码。

类型

为这些类型实现了 From<T>,以便于快速转换。

请注意,这些总是会执行快速移动操作。如果您希望 TinyBuf 尝试在长度足够小的情况下将数据复制到内联 TinyBuf::Array* 变体中,并执行标准的 .into() 操作,请使用 TinyBuf::copy_if_small

源类型 注释
[u8; N] 其中 N <= 23 内联存储,没有堆分配或数据复制。
Arc<dynAsRef<[u8]> + 发送 + 同步 + 静态>
盒子<dynAsRef<[u8]> + 发送 + 同步 + 静态>
盒子<[u8]> 这是一个独立的变体,因为将其转换为 Box<dyn AsRef<u8>> 需要进一步的封装。
&静态 [u8]
Vec<u8> 这是一个独立的变体,因为 into_boxed_slice 可能会重新分配。容量必须小于 2^56

克隆

当你调用 TinyVec::clone 时,它将尝试执行最佳复制。

  • 如果长度小于或等于 23,数据将被复制到一个新的 TinyBuf::Array* 变体中,即使它目前不是一个 TinyBuf::Array*
  • 如果是 TinyBuf::Arc,它将使用 Arc::clone 以低成本复制。
  • 否则,它将通过封装(即堆分配)复制到一个新的 TinyBuf::BoxSlice 中。

比较

Vec<u8>

TinyBuf 是一个不可变数据的容器,不支持任何变异方法,与 Vec<u8> 不同。

TinyBuf 在长度足够小的情况下将避免堆分配,而 Vec<u8> 除非是零字节,否则总是进行堆分配。

SmallVecTinyVec

TinyBuf 仅持有 u8 值,不支持变异。

TinyBuf 不需要提前声明长度或容量,并将动态优化长度小于或等于 23 的数组。

TinyBuf 可以从各种类型转换,不仅仅是 Vec<u8>,并且只使用移动,不进行数据复制。

一般来说,TinyBuf 更像是不可变 Box<dyn AsRef<[u8]>> 的替代品,而不是可变 Vec<u8>

&[u8]

如果可能使用切片,则首选。然而,当不可能使用时,通常是由于生命周期限制,TinyBuf 提供了几乎相同的功能。

Cow<u8>

Cow 仍然具有生命周期,因为它可能包含借用数据,所以它不像 Vec<u8> 和 TinyBuf 那样是所有者。

依赖项

~1.6–2.4MB
~46K SLoC