# Layout: Optimized memory layout using struct of array, Data-oriented design in Rust.
[](https://crates.io/crates/layout)
[](https://github.com/fereidani/layout)
[](https://github.com/fereidani/layout/actions)
[](https://docs.rs/layout)
This crate is a hard fork of [soa-derive](https://github.com/lumol-org/soa-derive) with `no_std` support and additional features.
This crate provides a custom derive (`#[derive(SOA)]`) to
automatically generate code from a given struct `T` that allow to replace
`Vec<T>` with a struct of arrays. For example, the following code
```rust
#[derive(SOA)]
pub struct Cheese {
pub smell: f64,
pub color: (f64, f64, f64),
pub with_mushrooms: bool,
pub name: String,
}
```
will generate a `CheeseVec` struct that looks like this:
```rust
pub struct CheeseVec {
pub smell: Vec<f64>,
pub color: Vec<(f64, f64, f64)>,
pub with_mushrooms: Vec<bool>,
pub name: Vec<String>,
}
```
It will also generate the same functions that a `Vec<Cheese>` would have, and a
few helper structs: `CheeseSlice`, `CheeseSliceMut`, `CheeseRef` and
`CheeseRefMut` corresponding respectivly to `&[Cheese]`, `&mut [Cheese]`,
`&Cheese` and `&mut Cheese`.
Any struct derived by SOA will auto impl trait `SOA`.
You can use `<Cheese as SOA>::Type` instead of the explicitly named type `CheeseVec`.
## How to use it
Add `#[derive(SOA)]` to each struct you want to derive a struct of
array version. If you need the helper structs to derive additional traits (such
as `Debug` or `PartialEq`), you can add an attribute `#[layout(Debug,
PartialEq)]` to the struct declaration.
```rust
#[derive(Debug, PartialEq, SOA)]
#[layout(Debug, PartialEq)]
pub struct Cheese {
pub smell: f64,
pub color: (f64, f64, f64),
pub with_mushrooms: bool,
pub name: String,
}
```
If you want to add attribute to a specific generated struct(such as
`#[cfg_attr(test, derive(PartialEq))]` on `CheeseVec`), you can add an
attribute `#[soa_attr(Vec, cfg_attr(test, derive(PartialEq)))]` to the
struct declaration.
```rust
#[derive(Debug, PartialEq, SOA)]
#[soa_attr(Vec, cfg_attr(test, derive(PartialEq)))]
pub struct Cheese {
pub smell: f64,
pub color: (f64, f64, f64),
pub with_mushrooms: bool,
pub name: String,
}
```
Mappings for first argument of `soa_attr` to the generated struct for `Cheese`:
- `Vec` => `CheeseVec`
- `Slice` => `CheeseSlice`
- `SliceMut` => `CheeseSliceMut`
- `Ref` => `CheeseRef`
- `RefMut` => `CheeseRefMut`
- `Ptr` => `CheesePtr`
- `PtrMut` => `CheesePtrMut`
## Usage and API
All the generated code have some generated documentation with it, so you
should be able to use `cargo doc` on your crate and see the documentation
for all the generated structs and functions.
Most of the time, you should be able to replace `Vec<Cheese>` by
`CheeseVec`, with exception of code using direct indexing in the vector and
a few other caveats listed below.
### Caveats and limitations
`Vec<T>` functionalities rely a lot on references and automatic _deref_ feature,
for getting function from `[T]` and indexing. But the SoA vector (let's call it
`CheeseVec`, generated from the `Cheese` struct) generated by this crate can not
implement `Deref<Target=CheeseSlice>`, because `Deref` is required to return a
reference, and `CheeseSlice` is not a reference. The same applies to `Index` and
`IndexMut` trait, that can not return `CheeseRef/CheeseRefMut`. This means that
the we can not index into a `CheeseVec`, and that a few functions are
duplicated, or require a call to `as_ref()/as_mut()` to change the type used.
## Iteration
It is possible to iterate over the values in a `CheeseVec`
```rust
let mut vec = CheeseVec::new();
vec.push(Cheese::new("stilton"));
vec.push(Cheese::new("brie"));
for cheese in vec.iter() {
// when iterating over a CheeseVec, we load all members from memory
// in a CheeseRef
let typeof_cheese: CheeseRef = cheese;
println!("this is {}, with a smell power of {}", cheese.name, cheese.smell);
}
```
Note that using `iter()` is nearly as fast as manually selecting fields (see below), as LLVM optimizes away unused field references in release builds.
One of the main advantage of the SoA layout is to be able to only load some
fields from memory when iterating over the vector. In order to do so, one
can manually pick the needed fields:
```rust
for name in &vec.name {
// We get referenes to the names
let typeof_name: &String = name;
println!("got cheese {}", name);
}
```
In order to iterate over multiple fields at the same time, one can use the
[soa_zip!](https://docs.rs/layout/*/layout/macro.soa_zip.html) macro.
```rust
for (name, smell, color) in soa_zip!(vec, [name, mut smell, color]) {
println!("this is {}, with color {:#?}", name, color);
// smell is a mutable reference
*smell += 1.0;
}
```
## Nested Struct of Arrays
In order to nest a struct of arrays inside another struct of arrays, one can use the `#[nested_soa]` attribute.
For example, the following code
```rust
#[derive(SOA)]
pub struct Point {
x: f32,
y: f32,
}
#[derive(SOA)]
pub struct Particle {
#[nested_soa]
point: Point,
mass: f32,
}
```
will generate structs that looks like this:
```rust
pub struct PointVec {
x: Vec<f32>,
y: Vec<f32>,
}
pub struct ParticleVec {
point: PointVec, // rather than Vec<Point>
mass: Vec<f32>
}
```
All helper structs will be also nested, for example `PointSlice` will be nested in `ParticleSlice`.
## Documentation
Please see http://lumol.org/layout/layout_example/ for a small
example and the documentation of all the generated code.
## Benchmarks
The benchmarks compare two different data layouts:
- **AoS (Array of Structures)**: A simple Rust `Vec<Structure>` where each element is a complete structure.
- **SoA (Structure of Arrays)**: Uses the struct-of-arrays layout provided by this crate, where each field is stored in a separate contiguous array.
**Results**: The SoA implementation shows up to **3x faster performance** for read operations compared to the traditional AoS approach.
```
test aos_big_do_work_100k ... bench: 161,151 ns/iter (+/- 57,573)
test aos_big_do_work_10k ... bench: 6,979 ns/iter (+/- 158)
test aos_big_push ... bench: 58 ns/iter (+/- 27)
test aos_small_do_work_100k ... bench: 66,672 ns/iter (+/- 599)
test aos_small_push ... bench: 16 ns/iter (+/- 7)
test soa_big_do_work_100k ... bench: 69,611 ns/iter (+/- 2,165)
test soa_big_do_work_10k ... bench: 6,708 ns/iter (+/- 117)
test soa_big_do_work_simple_100k ... bench: 76,656 ns/iter (+/- 1,675)
test soa_big_push ... bench: 42 ns/iter (+/- 4)
test soa_small_do_work_100k ... bench: 66,586 ns/iter (+/- 1,238)
test soa_small_push ... bench: 6 ns/iter (+/- 3)
```
Benchmarks tests exist for soa (struct of array) and aos (array of struct)
versions of the same code, using a small (24 bytes) and a big (240 bytes) struct.
You can run the same benchmarks on your own system by cloning this repository
and running `cargo bench`.
## Licensing and contributions
This crate distributed under either the MIT or the Apache license, at your
choice. Contributions are welcome, please open an issue before to discuss your
changes !
Thanks to @maikklein for the initial idea: https://maikklein.github.io/soa-rust/