10个版本 (4个重大更改)
新增 0.7.2 | 2024年8月20日 |
---|---|
0.7.1 | 2024年8月11日 |
0.6.1 | 2024年5月9日 |
0.5.1 | 2024年5月4日 |
0.3.1 | 2024年2月28日 |
#296 在 Rust模式
每月385次下载
115KB
2K SLoC
soa-rs
soa-rs简化了与数组结构内存布局的交互。与数组结构(AoS)对应的是结构数组(SoA)。
示例
use soa_rs::{Soars, soa, AsSlice};
// Derive soa-rs for your type
#[derive(Soars, PartialEq, Debug)]
#[soa_derive(Debug, PartialEq)]
struct Baz {
foo: u16,
bar: u8,
}
// Create the SoA
let mut soa = soa![
Baz { foo: 1, bar: 2 },
Baz { foo: 3, bar: 4 },
];
// Each field has a slice
assert_eq!(soa.foo(), [1, 3]);
assert_eq!(soa.bar(), [2, 4]);
// Tuple structs work too
#[derive(Soars, PartialEq, Debug)]
#[soa_derive(Debug, PartialEq)]
struct Tuple(u16, u8);
let tuple = soa![Tuple(1, 2), Tuple(3, 4), Tuple(5, 6), Tuple(7, 8)];
// SoA can be sliced and indexed like normal slices
assert_eq!(tuple.idx(1..3), soa![Tuple(3, 4), Tuple(5, 6)]);
assert_eq!(tuple.idx(3), TupleRef(&7, &8));
// Drop-in for Vec in many cases
soa.insert(0, Baz { foo: 5, bar: 6 });
assert_eq!(soa.pop(), Some(Baz { foo: 3, bar: 4 }));
assert_eq!(soa, soa![Baz { foo: 5, bar: 6 }, Baz { foo: 1, bar: 2 }]);
for mut el in &mut soa {
*el.foo += 10;
}
assert_eq!(soa, soa![Baz { foo: 15, bar: 6 }, Baz { foo: 11, bar: 2}]);
什么是SoA?
与AoS将类型的所有字段存储在每个数组的元素中不同,SoA将每个字段分割成其自己的数组。例如,考虑以下结构体:
struct Example {
foo: u8,
bar: u64,
}
为了获得适当的内存对齐,此结构体将具有以下布局。在这个极端示例中,几乎一半的内存被浪费在填充中。
╭───┬───────────────────────────┬───────────────────────────────╮
│foo│ padding │ bar │
╰───┴───────────────────────────┴───────────────────────────────╯
通过使用SoA,字段将被单独存储,从而消除了填充的需求
╭───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬┄
│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│
╰───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴┄
╭───────────────────────────────┬───────────────────────────────┬┄
│ bar │ bar │
╰───────────────────────────────┴───────────────────────────────┴┄
性能
除了降低内存使用外,还有几个原因说明SoA可以提供更好的性能
- 通过消除填充,每个缓存行通常具有更高的信息密度。
- 当只访问可用字段的一个子集时,只会检索那些字段的数值。
SoA并非在所有情况下都能提供性能提升。特别是,如push
和pop
这样的操作通常比Vec
要慢,因为每个字段的内存都相距甚远。SoA最适用于以下情况之一:
- 顺序访问是常见的访问模式
- 您经常只访问或修改字段的一个子集
SIMD向量化
SoA使得将数据存入和取出SIMD寄存器变得非常简单。由于值是顺序存储的,因此加载数据就像将内存范围读取到寄存器中一样简单。这种大量数据传输非常适合自动向量化。相比之下,AoS在内存中分散存储字段。因此,必须将单个字段分别复制到寄存器中的不同位置,稍后以相同的方式重新排序。这可能会阻止编译器应用向量化。因此,SoA更有可能从SIMD优化中受益。
示例
Zig
SoA(面向数据的设计)是一种流行的设计技术。Andrew Kelley在这个演讲中,非常精彩地描述了SoA和其他面向数据的设计模式如何帮助他在Zig编译器中实现了39%的编译时间减少。
基准测试
soa-rs-testing
包含了一个比较基准,用于计算2¹⁶个4D向量的点积。Vec
版本运行时间为132µs,而Soa
版本运行时间为22µs,提高了6倍。
比较
soa_derive
soa_derive
将每个字段转换为它自己的Vec
。因此,每个字段的长度、容量和分配都是独立管理的。相比之下,soa-rs为每个Soa
管理一个单独的分配。此外,soa_derive
还为每个结构体生成一个新的集合类型,而soa-rs生成一个最小化、低级的接口,该接口用于通用Soa
类型的实现。这提供了更多的类型系统灵活性、更少的代码生成和更好的文档。
soa-vec
与soa-vec
仅在nightly编译器上编译不同,soa-rs也可以在stable编译器上编译。与使用derive宏不同,soa-vec
使用宏生成其SoA类型的八个静态副本,具有固定的元组大小。
依赖项
~0.3–0.8MB
~19K SLoC