#soa #macro-derive #fields #array #slice #soa-rs #soars

soa-rs-derive

为soa-rs提供的进程宏派生工具

5个版本 (3个破坏性版本)

新版本 0.7.0 2024年8月20日
0.6.0 2024年5月9日
0.4.0 2024年3月21日
0.3.1 2024年2月28日
0.3.0 2024年2月27日

#8#soa

Download history 154/week @ 2024-05-04 24/week @ 2024-05-11 7/week @ 2024-05-18 9/week @ 2024-05-25 6/week @ 2024-06-01 3/week @ 2024-06-08 2/week @ 2024-06-15 3/week @ 2024-06-29 4/week @ 2024-07-06 7/week @ 2024-07-20 23/week @ 2024-07-27 1/week @ 2024-08-03 1/week @ 2024-08-10 152/week @ 2024-08-17

每月177次下载
soa-rs 中使用

MIT 许可证

46KB
891

docs.rs Crates.io Version GitHub License

soa-rs

soa-rs使与数组结构内存布局一起工作变得简单。Vec 相当于结构体数组 (AoS),而 Soa 相当于数组结构体 (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%的wall clock时间。

基准测试

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上编译。soa-vec不是使用derive宏,而是使用宏生成SoA类型的八个固定元组大小的静态副本。

依赖

~315–780KB
~19K SLoC