6个版本

0.2.8 2024年2月27日
0.2.7 2024年2月19日
0.2.0 2024年1月5日
0.1.0 2023年12月29日

#7 in #soa


soapy 中使用

MIT 协议

36KB
720

docs.rs Crates.io Version GitHub License

Soapy

Soapy使处理数组结构内存布局变得简单。Vec是数组结构(AoS),而Soa是数组结构(SoA)。

示例

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并不在所有情况下都能提供性能提升。特别是,像pushpop这样的操作通常比Vec慢,因为每个字段的内存相隔很远。SoA最适合以下情况:

  • 顺序访问是常见的访问模式
  • 你经常只访问或修改字段的一个子集

SIMD向量化

SoA使数据进入和离开SIMD寄存器变得简单。由于值是顺序存储的,加载数据就像将内存中的范围读取到寄存器中一样简单。这种大量数据传输非常适合自动向量化。相比之下,AoS在内存中将字段存储在不同的位置。因此,必须将单个字段分别复制到寄存器中的不同位置,然后在相同的方式下将其移回。这可能会阻止编译器应用向量化。因此,SoA更有可能从SIMD优化中受益。

示例

Zig

SoA是面向数据设计中的一种流行技术。Andrew Kelley给出了一个精彩的演讲,描述了SoA和其他面向数据设计模式如何帮助他在Zig编译器中减少了39%的wall clock时间。

基准测试

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 类型八个静态副本,具有固定的元组大小。

依赖项

~305–760KB
~18K SLoC