73个版本
0.0.123 | 2024年7月11日 |
---|---|
0.0.122 | 2024年5月17日 |
0.0.121 | 2024年4月28日 |
0.0.108 | 2024年3月30日 |
0.0.86 | 2023年10月29日 |
#1039 in 编码
12,270 monthly downloads
用于 musli-tests
530KB
9K SLoC
musli-zerocopy
由Müsli提供的清新简单、速度极快的零拷贝原语。
这提供了一套基本工具来处理在反序列化过程中不需要复制的类型。您定义类型 T
,我们提供安全的 &[u8]
<-> &T
转换。
读取零拷贝结构具有完整的 #[no_std]
支持。目前构建一个结构需要启用 alloc
功能。
use musli_zerocopy::{buf, Ref, ZeroCopy};
#[derive(ZeroCopy)]
#[repr(C)]
struct Person {
age: u8,
name: Ref<str>,
}
let buf = buf::aligned_buf::<Person>(include_bytes!("author.bin"));
let person = Person::from_bytes(&buf[..])?;
assert_eq!(person.age, 35);
// References are incrementally validated.
assert_eq!(buf.load(person.name)?, "John-John");
有关如何工作的详细概述,请参阅 ZeroCopy
及其相应的 ZeroCopy
derive。下面还有一份高级指南。
此crate还包括一些您可能感兴趣的高级数据结构
phf
基于phf
crate 提供映射和集合,或完美哈希函数。swiss
是hashbrown
crate 的移植,它是一个Google SwissTable实现。trie
是前缀字典树的实现,支持高效的字节前缀多值查找。
如果您对 musli-zerocopy
的性能感兴趣,请访问 benchmarks
。我将继续扩展此套件以包含更多零拷贝类型,但到目前为止,我们在测试过的用例中已经取得了明显的领先。
这是因为...
- 零拷贝如果在正确执行的情况下,则不会产生反序列化开销。你从某个地方获取字节,验证它们,并将它们视为目标类型。这种做法的方法有限;
- 填充已经实现并进行了优化,使其生成的代码大致等同于你手动编写的代码,并且;
- 增量验证意味着你只需为所访问的内容付费。因此,对于随机访问,我们只需验证正在访问的部分。
概述
为什么我应该考虑musli-zerocopy而不是X?
由于这是每个人都会问的第一个问题,以下是与其他流行库的不同之处
zerocopy
不支持填充类型[^padded],字节到引用的转换,或者不允许解码类型的转换,除非所有位模式都可以由零填充[^zeroes]。我们仍然希望提供一个更完整的工具包,用于构建和交互像通过phf
和swiss
模块一样复杂的数据结构。这个crate确实可能在某个时候使用zerocopy
的特性和方法。rkyv
操作于&[u8]
类型,并为你生成一个优化的Archived
变体。这个库让你可以直接构建相当于Archived
变体的东西,并且你与数据模型交互的方式不会产生验证的开销。使用rkyv
,我的电脑需要100%的CPU核心,大约半秒钟的时间来加载1200万个字典条目^dictionary,这是一个增量验证所不产生的高昂成本。不验证不是一种选择,因为那将非常不稳定——你的应用程序将容易受到恶意字典文件的影响。
[^padded]: 这在zerocopy的路线图上,但与核心的FromBytes
/ ToBytes
特性对基本不兼容。
[^zeroes]: FromBytes扩展了FromZeroes
指南
本库中的零拷贝指的是与直接位于&[u8]
内存中的数据结构交互,而无需先解码它们的行为。
从概念上讲,它的工作方式有点像这样。
假设你想要存储字符串"Hello World!"
。
use musli_zerocopy::OwnedBuf;
let mut buf = OwnedBuf::new();
let string = buf.store_unsized("Hello World!");
let reference = buf.store(&string);
assert_eq!(reference.offset(), 12);
这将导致以下缓冲区
0000: "Hello World!"
// Might get padded to ensure that the size is aligned by 4 bytes.
0012: offset -> 0000
0016: size -> 12
我们在偏移量0016
处看到的是8字节的Ref<str>
。第一个字段存储了获取字符串的偏移量,第二个字段存储了字符串的长度。
让我们看一下一个 Ref<[u32]>
的例子。
use musli_zerocopy::{Ref, OwnedBuf};
let mut buf = OwnedBuf::new();
let slice: Ref<[u32]> = buf.store_slice(&[1, 2, 3, 4]);
let reference = buf.store(&slice);
assert_eq!(reference.offset(), 16);
这将导致以下缓冲区
0000: u32 -> 1
0004: u32 -> 2
0008: u32 -> 3
0012: u32 -> 4
0016: offset -> 0000
0020: length -> 4
在地址 0016
处我们存储了两个字段,它们对应于一个 Ref<[u32]>
。
接下来,让我们通过一个使用 Custom
结构的例子来调查一下。
use core::mem::size_of;
use musli_zerocopy::{OwnedBuf, Ref, ZeroCopy};
#[derive(ZeroCopy)]
#[repr(C)]
struct Custom {
field: u32,
string: Ref<str>,
}
let mut buf = OwnedBuf::new();
let string = buf.store_unsized("Hello World!");
let custom = buf.store(&Custom { field: 42, string });
// The buffer stores both the unsized string and the Custom element.
assert!(buf.len() >= 24);
// We assert that the produced alignment is smaller or equal to 8
// since we'll be relying on this below.
assert!(buf.requested() <= 8);
这将导致以下缓冲区
0000: "Hello World!"
0012: u32 -> 42
0016: offset -> 0000
0020: size -> 12
我们的结构从地址 0012
开始,首先有一个 u32
字段,紧接着是字符串。
读取数据
稍后当我们想要使用这个类型时,我们将生成的缓冲区包含在其他地方。
我们需要一些数据(我们称之为 DNA)来从原始缓冲区中读取类型
- 缓冲区的 对齐。您可以通过
requested()
来读取。在接收端,我们需要确保缓冲区遵循此对齐。动态上,可以通过使用aligned_buf(bytes, align)
来实现。其他技巧包括在对齐的新类型中嵌入静态缓冲区,我们将在下面展示。网络应用程序可能简单地同意使用特定的对齐方式。这种对齐方式必须与被强制转换的类型兼容。 - 产生缓冲区的机器的 字节序。任何数值元素都将按照本地字节序排列,因此如果它们不同,就必须在读取端进行调整。
- 正在读取的类型定义,它实现了
ZeroCopy
。这是上面的Custom
。通过使用ZeroCopy
derive,我们确保可以安全地将缓冲区强制转换为类型的引用。最坏的情况是数据可能被破坏,但我们永远不会在使用安全 API 时做出不安全的行为。 - 读取
ZeroCopy
结构的偏移量。为了读取一个结构,我们将指针和类型组合成一个Ref
实例。
如果目标是同时在同一系统上生成和读取缓冲区,可以做出一些假设。如果这些假设被证明是错误的,那么最坏的结果也只会是错误,只要您使用安全 API 或遵守不安全 API 的安全性文档。
信息 关于通过网络发送数据的一些说明。只要包括缓冲区的对齐和数据结构的大小端,这是完全可以做到的。
# use musli_zerocopy::OwnedBuf; let buf = OwnedBuf::new(); /* write something */ let is_little_endian = cfg!(target_endian = "little"); let alignment = buf.requested();
以下是从新类型对齐的 &'static [u8]
缓冲区直接读取类型的例子
use core::mem::size_of;
use musli_zerocopy::Buf;
// Helper to force the static buffer to be aligned like `A`.
#[repr(C)]
struct Align<A, T: ?Sized>([A; 0], T);
static BYTES: &Align<u64, [u8]> = &Align([], *include_bytes!("custom.bin"));
let buf = Buf::new(&BYTES.1);
// Construct a pointer into the buffer.
let custom = Ref::<Custom>::new(BYTES.1.len() - size_of::<Custom>());
let custom: &Custom = buf.load(custom)?;
assert_eq!(custom.field, 42);
assert_eq!(buf.load(custom.string)?, "Hello World!");
在零偏移量处写入数据
大多数情况下,您希望将数据写入缓冲区,其中第一个元素是当前正在写入的元素。
这很有用,因为它满足上述最后一个要求,即结构体可以读取的偏移量将是零,并且它所依赖的所有数据都存储在随后的偏移量中。
use musli_zerocopy::OwnedBuf;
use musli_zerocopy::mem::MaybeUninit;
let mut buf = OwnedBuf::new();
let reference: Ref<MaybeUninit<Custom>> = buf.store_uninit::<Custom>();
let string = buf.store_unsized("Hello World!");
buf.load_uninit_mut(reference).write(&Custom { field: 42, string });
let reference = reference.assume_init();
assert_eq!(reference.offset(), 0);
可移植性
默认情况下,归档将使用本机 ByteOrder
。为了构建和加载可移植归档,必须显式指定使用的字节顺序。
这通过指定缓冲区构建过程中使用的字节顺序,并显式设置接收它的类型中的 E
参数来实现,例如 Ref<T, E, O>
。
我们可以从定义一个完全 Portable
归档结构开始,该结构接收大小和 ByteOrder
。请注意,也可以明确指定所需的字节顺序,但这样做可以使其作为一个例子具有最大的灵活性
use musli_zerocopy::{Size, ByteOrder, Ref, Endian, ZeroCopy};
#[derive(ZeroCopy)]
#[repr(C)]
struct Archive<E, O>
where
E: ByteOrder,
O: Size
{
string: Ref<str, E, O>,
number: Endian<u32, E>,
}
从结构体中构建缓冲区相当直接,OwnedBuf
有一个 with_byte_order::<E>()
方法,允许我们指定在构建过程中与之交互的类型中使用的“粘性” ByteOrder
。
use musli_zerocopy::{endian, Endian, OwnedBuf};
let mut buf = OwnedBuf::new()
.with_byte_order::<endian::Little>();
let first = buf.store(&Endian::le(42u16));
let portable = Archive {
string: buf.store_unsized("Hello World!"),
number: Endian::new(10),
};
let portable = buf.store(&portable);
assert_eq!(&buf[..], &[
42, 0, // 42u16
72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, // "Hello World!"
0, 0, // padding
2, 0, 0, 0, 12, 0, 0, 0, // Ref<str>
10, 0, 0, 0 // 10u32
]);
let portable = buf.load(portable)?;
let mut buf = OwnedBuf::new()
.with_byte_order::<endian::Big>();
let first = buf.store(&Endian::be(42u16));
let portable = Archive {
string: buf.store_unsized("Hello World!"),
number: Endian::new(10),
};
let portable = buf.store(&portable);
assert_eq!(&buf[..], &[
0, 42, // 42u16
72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, // "Hello World!"
0, 0, // padding
0, 0, 0, 2, 0, 0, 0, 12, // Ref<str>
0, 0, 0, 10 // 10u32
]);
let portable = buf.load(portable)?;
限制
偏移量、无大小值的尺寸和切片长度都限制在32位。您所使用的系统必须有至少32位宽的 usize
类型。这是默认情况下节省空间的做法。
系统上的指针宽度在编译时进行检查,尝试使用大于 2^32
的偏移量或大小将导致panic。
大于 2^32
的地址使用示例导致panic
Ref::<Custom>::new(1usize << 32);
使用长度大于 2^32
的 Ref<\[T\]>
导致panic的示例
Ref::<[Custom]>::with_metadata(0, 1usize << 32);
使用大小大于 2^32
的 Ref<str>
值导致panic的示例
Ref::<str>::with_metadata(0, 1usize << 32);
如果您想访问超出此限制的数据,建议将数据集划分为32位地址可寻址的块。
如果您确实想更改此限制,可以通过设置各种 Size
依赖类型的默认 O
参数来修改它
可用的 Size
实现包括
u32
用于32位大小的指针(默认)。usize
用于目标相关大小的指针。
// These no longer panic:
let reference = Ref::<Custom, Native, usize>::new(1usize << 32);
let slice = Ref::<[Custom], Native, usize>::with_metadata(0, 1usize << 32);
let unsize = Ref::<str, Native, usize>::with_metadata(0, 1usize << 32);
要使用自定义的 Size
初始化 OwnedBuf
,您可以使用 OwnedBuf::with_size
use musli_zerocopy::OwnedBuf;
use musli_zerocopy::buf::DefaultAlignment;
let mut buf = OwnedBuf::with_capacity_and_alignment::<DefaultAlignment>(0)
.with_size::<usize>();
在 OwnedBuf
的构建过程中指定的 Size
将传递到它返回的任何指针中
use musli_zerocopy::{DefaultAlignment, OwnedBuf, Ref, ZeroCopy};
use musli_zerocopy::endian::Native;
#[derive(ZeroCopy)]
#[repr(C)]
struct Custom {
reference: Ref<u32, Native, usize>,
slice: Ref::<[u32], Native, usize>,
unsize: Ref::<str, Native, usize>,
}
let mut buf = OwnedBuf::with_capacity(0)
.with_size::<usize>();
let reference = buf.store(&42u32);
let slice = buf.store_slice(&[1, 2, 3, 4]);
let unsize = buf.store_unsized("Hello World");
buf.store(&Custom { reference, slice, unsize });
依赖关系
~0.5-1MB
~23K SLoC