11个版本
0.2.9 | 2024年2月27日 |
---|---|
0.2.8 | 2024年2月27日 |
0.2.2 | 2024年1月8日 |
0.1.0 | 2023年12月29日 |
在#soa分类中排名2
每月下载120次
105KB
2K SLoC
Soapy
Soapy使处理数组结构内存布局变得简单。Vec
示例
use soapy::{Soapy, soa};
// Derive Soapy for your type
#[derive(Soapy, 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(Soapy, 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), [Tuple(3, 4), Tuple(5, 6)]);
assert_eq!(tuple.idx(3), Tuple(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, [Baz { foo: 5, bar: 6 }, Baz { foo: 1, bar: 2 }]);
for mut el in &mut soa {
*el.foo += 10;
}
assert_eq!(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%的wall clock time。
基准测试
soapy-testing
包含一个比较基准,该基准计算 2¹⁶ 个 4D 向量的点积总和。使用 Vec
版本运行时间为 132µs,而 Soa
版本运行时间为 22µs,性能提高了 6 倍。
比较
soa_derive
soa_derive
将每个字段都视为其自己的 Vec
。因此,每个字段的长度、容量和分配都是单独管理的。相比之下,Soapy 为每个 Soa
管理单个分配。此外,soa_derive
为每个结构体生成一个新的集合类型,而 Soapy 生成一个最小化、底层的接口,该接口由泛型 Soa
类型用于其实施。这提供了更多类型系统灵活性、更少的代码生成和更好的文档。
soa-vec
虽然 soa-vec
只能在 nightly 版本上编译,但 Soapy 也可以在 stable 版本上编译。与使用 derive 宏不同,soa-vec
使用宏生成其 SoA 类型的八个静态副本,具有固定的元组大小。
依赖关系
~260–710KB
~17K SLoC