Giter Club home page Giter Club logo

soa-rs's Introduction

docs.rs Crates.io Version GitHub License

soa-rs

soa-rs makes it simple to work with the structure-of-arrays memory layout. What Vec<T> is to array-of-structures (AoS), Soa<T> is to structure-of-arrays (SoA).

Example

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}]);

What is SoA?

Whereas AoS stores all the fields of a type in each element of the array, SoA splits each field into its own array. For example, consider

struct Example {
    foo: u8,
    bar: u64,
}

In order to have proper memory alignment, this struct will have the following layout. In this extreme example, almost half of the memory is wasted to padding.

╭───┬───────────────────────────┬───────────────────────────────╮
│foo│         padding           │              bar              │
╰───┴───────────────────────────┴───────────────────────────────╯

By using SoA, the fields will be stored separately, removing the need for padding:

╭───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬┄
│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│foo│
╰───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴┄
╭───────────────────────────────┬───────────────────────────────┬┄
│             bar               │              bar              │
╰───────────────────────────────┴───────────────────────────────┴┄

Performance

In addition to lowering memory usage, there are several reasons why SoA can offer better performance:

  • By removing padding, each cacheline is typically more information-dense.
  • When accessing only a subset of the available fields, only data for those fields will be fetched.

SoA does not offer performance wins in all cases. In particular, operations such as push and pop are usually slower than for Vec since the memory for each field is far apart. SoA is most appropriate when either

  • Sequential access is the common access pattern
  • You are frequently accessing or modifying only a subset of the fields

SIMD vectorization

SoA makes getting data into and out of SIMD registers trivial. Since values are stored sequentially, loading data is as simple as reading a range of memory into the register. This bulk data transfer is very amenable to auto-vectorization. In contrast, AoS stores fields at disjoint locations in memory. Therefore, individual fields must be individually copied to different positions within the registers and, later, shuffled back out in the same way. This can prevent the compiler from applying vectorization. For this reason, SoA is much more likely to benefit from SIMD optimizations.

Examples

Zig

SoA is a popular technique in data-oriented design. Andrew Kelley gives a wonderful talk describing how SoA and other data-oriented design patterns earned him a 39% reduction in wall clock time in the Zig compiler.

Benchmark

soa-rs-testing contains a benchmark comparison that sums the dot products of 2¹⁶ 4D vectors. The Vec version runs in 132µs and the Soa version runs in 22µs, a 6x improvement.

Comparison

soa_derive makes each field its own Vec. Because of this, each field's length, capacity, and allocation are managed separately. In contrast, soa-rs manages a single allocation for each Soa. soa_derive also generates a new collection type for every struct, whereas soa-rs generates a minimal, low-level interface that the generic Soa type uses for its implementation. This provides more type system flexibility, less code generation, and better documentation.

Whereas soa-vec only compiles on nightly, soa-rs also compiles on stable. Rather than using derive macros, soa-vec instead uses macros to generate eight static copies of their SoA type with fixed tuple sizes.

soa-rs's People

Contributors

ppakalns avatar tim-harding avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

soa-rs's Issues

So, the whole business of exposing `Slice` through `&mut Slice<T>` and `&Slice<T>` seems problematic…

…i.e. never forget the existence of mem::swap. Here’s a problematic code example

use soapy::{Soa, Soapy};

#[derive(Soapy)]
struct Foo(u8);

fn main() {
    let mut x = Soa::<Foo>::new();
    x.push(Foo(0));
    let mut y = Soa::<Foo>::new();

    std::mem::swap(x.as_mut_slice(), y.as_mut_slice());
    x.push(Foo(0)); // segfault
}

Unless of course Soa was somehow made to deal with blatantly incorrect capacity values… which seems impossible because capacity is safety-critical information, e.g. instead of out-of-bounds access like above (which can be checked by looking at the len), access of uninitialized data is the whole point of what capacity tries to prevent.

This unfortunately kills the deref-based API, at least as far as DerefMut is concerned, at least you can’t move out of a shared reference so the Deref part does still stand?


Nevermind. As I’m writing this, I’m seeing the next problem:

use soapy::{Soa, Soapy};

#[derive(Soapy)]
struct Foo(Box<&'static &'static usize>);

fn main() {
    let mut x = Soa::<Foo>::new();
    x.push(Foo(Box::new(&&0)));
    let slice = *x.as_slice();

    drop(x);
    println!("{:?}", slice.get(0).unwrap().0); // segfault
}

the type Slice<T> is Copy, and thus can just be copied out from behind even a shared immutable reference, kept without a lifetime, and lead to use-after free.

Compile error from `soa-rs-derive`: can't leak private type

Using version 0.3.1 of soa-rs I cannot compile anything because a private type is being leaked from soa-rs-derive.

For me, to reproduce the issue it's as simple as

cargo init test_soa_rs;
cd test_soa_rs;
cargo add soa-rs;
cargo check;

I get the following error message

   Compiling soa-rs-derive v0.3.1
error[E0446]: private type `SoaDerive` in public interface
  --> C:\Users\zane\.cargo\registry\src\index.crates.io-6f17d22bba15001f\soa-rs-derive-0.3.1\src\fields.rs:9:1
   |
9  | / pub fn fields_struct(
10 | |     ident: Ident,
11 | |     vis: Visibility,
12 | |     fields: Punctuated<Field, Comma>,
13 | |     kind: FieldKind,
14 | |     soa_derive: SoaDerive,
15 | | ) -> Result<TokenStream, syn::Error> {
   | |____________________________________^ can't leak private type
   |
  ::: C:\Users\zane\.cargo\registry\src\index.crates.io-6f17d22bba15001f\soa-rs-derive-0.3.1\src\lib.rs:75:1
   |
75 |   struct SoaDerive {
   |   ---------------- `SoaDerive` declared as private

For more information about this error, try `rustc --explain E0446`.
error: could not compile `soa-rs-derive` (lib) due to previous error

Feature suggestion: Array of enum variants

I know you are probably busy with the soundness issues right now, so this is going to to be low priority.

I recently ran into an issue where I had wildly varying sizes for different enum members. I could of course box one of them if it is rare, but I had a pretty even distribution in my case. Could we derive a "array of enum variants" similar to this that simply puts all instances of different enum types in different arrays internally, thus reducing memory usage and improving cache locality? I don't know what a good API would look like, some thought is required here.

Support serde serialization, deserialization

Support serde serialization and deserialization of soa_rs::Soa.

Implementation could be added if #[soa_serde] attribute is added to the struct. It would require that all fields are serializable, deserializable.

This would be useful when data needs to be passed around and no conversion between Vec and soa_rs::Soa would be needed.

Generated `WithRef` implementations can’t be sound when interior mutability is at play

In other words, you can’t just copy a value to a different place, and then expose that copy by-immutable-reference to untrusted (safe) user code, because immutable references could have mutable access behind interior mutability.

use soapy::{Soa, Soapy, WithRef};
use std::cell::RefCell;

#[derive(Soapy)]
struct Foo(RefCell<String>);

fn main() {
    let mut x = Soa::<Foo>::new();
    x.push(Foo(RefCell::new(String::from("Hello World"))));

    let r = x.get(0).unwrap();

    // mem::take gets ownership and modifies the `String`, however these modifications are only happening
    // to the local copy via `ptr::read` in the `unsafe` implementation of `WithRef`, so the original `String`
    // keeps ownership, too
    let s1: String =
        r.with_ref(|copy_of_refcell| std::mem::take::<String>(&mut copy_of_refcell.0.borrow_mut()));

    // let's just do it again, because copy-paste is easy
    let s2: String =
        r.with_ref(|copy_of_refcell| std::mem::take::<String>(&mut copy_of_refcell.0.borrow_mut()));

    println!("{s1} and {s2} again?"); // ==>> "Hello World and Hello World again?"
    drop(s1);
    println!("freed!");
    drop(s2); // double-free
    println!("freed again!");

    // (triple-free, once `x` is dropped)
}

a workaround of “just write back the changes” of course can’t work either, as that isn’t thread-safe, whilst Soa promises Sync.

I don’t have any immediate ideas on how to keep the nice implementations of Hash and Eq and the like.

Support fields with types that do not implement PartialEq

Test that doesn't pass:

#[test]
fn field_type_without_partial_eq() {
    // Type that doesn't implement PartialEq
    struct TypeWithoutPartialEq {
        foo: u8,
    }

    #[derive(Soars)]
    struct Example {
        a: TypeWithoutPartialEq,
    }
    let arr = soa![Example {
        a: TypeWithoutPartialEq { foo: 1 }
    }];
    assert_eq!(arr.get(0).unwrap().a.foo, 1);
}

If PartialEq derives are added, then this test passes. It shouldn't be required.

Tried to fix this in https://github.com/tim-harding/soa-rs/pull/8/files , but pull request was closed even if the existing PartialEq implementation in derive macro for #itemRef was made conditional (kept the same) based on soa_derive.

This heavily limits what types can be stored in Soa.

Usage of `mem::forget` can interact badly with stacked borrows.

use soa_rs::{SoaArray, Soars};

fn main() {
    let foo = Box::new(42_u8);
    let x = ExampleArray::from_array([Example { foo }]);
    let s = x.as_slice();
    let v: &Box<u8> = s.get(0).unwrap().foo;
    dbg!(v);
}

#[derive(Soars)]
#[soa_derive(PartialEq)] // <- unrelated issue: I get a compile error without this
struct Example {
    foo: Box<u8>,
}
$ cargo +nightly miri run
Preparing a sysroot for Miri (target: aarch64-unknown-linux-gnu)... done
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `/home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/bin/cargo-miri runner target/miri/aarch64-unknown-linux-gnu/debug/play`
error: Undefined Behavior: trying to retag from <1690> for Unique permission at alloc752[0x0], but that tag does not exist in the borrow stack for this location
  --> src/main.rs:11:10
   |
11 | #[derive(Soars)]
   |          ^^^^^
   |          |
   |          trying to retag from <1690> for Unique permission at alloc752[0x0], but that tag does not exist in the borrow stack for this location
   |          this error occurs as part of retag (of a reference/box inside this compound value) at alloc752[0x0..0x1]
   |          errors for retagging in fields are fairly new; please reach out to us (e.g. at <https://rust-lang.zulipchat.com/#narrow/stream/269128-miri>) if you find this error troubling
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <1690> was created by a Unique retag at offsets [0x0..0x1]
  --> src/main.rs:11:10
   |
11 | #[derive(Soars)]
   |          ^^^^^
help: <1690> was later invalidated at offsets [0x0..0x1] by a Unique retag (of a reference/box inside this compound value)
  --> src/main.rs:11:10
   |
11 | #[derive(Soars)]
   |          ^^^^^
   = note: BACKTRACE (of the first span):
   = note: inside `ExampleArray::<1>::from_array` at src/main.rs:11:10: 11:15
note: inside `main`
  --> src/main.rs:5:13
   |
5  |     let x = ExampleArray::from_array([Example { foo }]);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: this error originates in the derive macro `Soars` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error
Better error with expanded `from_array`:
use soa_rs::{SoaArray, Soars};

fn main() {
    let foo = Box::new(42_u8);
    let x = ExampleArray::from_array_([Example { foo }]);
    let s = x.as_slice();
    let v: &Box<u8> = s.get(0).unwrap().foo;
    dbg!(v);
}

#[derive(Soars)]
#[soa_derive(PartialEq)] // <- unrelated issue: I get a compile error without this
struct Example {
    foo: Box<u8>,
}

impl<const N: usize> ExampleArray<N> {
    const fn from_array_(array: [Example; N]) -> Self {
        struct Uninit<const K: usize> {
            #[automatically_derived]
            foo: [::std::mem::MaybeUninit<Box<u8>>; K],
        };
        let mut uninit: Uninit<N> = Uninit {
            foo: unsafe { ::std::mem::MaybeUninit::uninit().assume_init() },
        };
        let mut i = 0;
        while i < N {
            let src = &array[i].foo as *const Box<u8>;
            unsafe {
                uninit.foo[i] = ::std::mem::MaybeUninit::new(src.read());
            }
            i += 1;
        }
        ::std::mem::forget(array);
        Self {
            foo: unsafe { ::std::mem::transmute_copy(&::std::mem::ManuallyDrop::new(uninit.foo)) },
        }
    }
}
cargo +nightly miri run
Preparing a sysroot for Miri (target: aarch64-unknown-linux-gnu)... done
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
     Running `/home/frank/.rustup/toolchains/nightly-aarch64-unknown-linux-gnu/bin/cargo-miri runner target/miri/aarch64-unknown-linux-gnu/debug/play`
warning: unnecessary trailing semicolon
  --> src/main.rs:22:10
   |
22 |         };
   |          ^ help: remove this semicolon
   |
   = note: `#[warn(redundant_semicolons)]` on by default

warning: `#[automatically_derived]` only has an effect on implementation blocks
  --> src/main.rs:20:13
   |
20 |             #[automatically_derived]
   |             ^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_attributes)]` on by default

error: Undefined Behavior: trying to retag from <1690> for Unique permission at alloc752[0x0], but that tag does not exist in the borrow stack for this location
  --> src/main.rs:36:27
   |
36 |             foo: unsafe { ::std::mem::transmute_copy(&::std::mem::ManuallyDrop::new(uninit.foo)) },
   |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                           |
   |                           trying to retag from <1690> for Unique permission at alloc752[0x0], but that tag does not exist in the borrow stack for this location
   |                           this error occurs as part of retag (of a reference/box inside this compound value) at alloc752[0x0..0x1]
   |                           errors for retagging in fields are fairly new; please reach out to us (e.g. at <https://rust-lang.zulipchat.com/#narrow/stream/269128-miri>) if you find this error troubling
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <1690> was created by a Unique retag at offsets [0x0..0x1]
  --> src/main.rs:30:33
   |
30 |                 uninit.foo[i] = ::std::mem::MaybeUninit::new(src.read());
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: <1690> was later invalidated at offsets [0x0..0x1] by a Unique retag (of a reference/box inside this compound value)
  --> src/main.rs:34:28
   |
34 |         ::std::mem::forget(array);
   |                            ^^^^^
   = note: BACKTRACE (of the first span):
   = note: inside `ExampleArray::<1>::from_array_` at src/main.rs:36:27: 36:97
note: inside `main`
  --> src/main.rs:5:13
   |
5  |     let x = ExampleArray::from_array_([Example { foo }]);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 2 warnings emitted

The problem is the usage of mem::forget in ExampleArray::from_array. This moves a value of type Example, and when you move / pass a Box<T> or &mut T in Rust (under Stacked Borrows at least) by value (even in a field), it asserts unique access to that reference. But at that point, the copy via src.read() is already done.

This should be easy to fix by using ManuallyDrop instead.

It is generally preferrable to use ManuallyDrop instead of mem::forget. (Looking at the code, this seems to be the only mem::forget you use anywhere though.) Wrap the struct you want to not drop (like array in the derive-generated fn from_array) into ManuallyDrop in the beginning of the function (i.e. in general: as soon as it’s clear you’re not gonna drop it, and before you copied any contents out of it), and then read the contents piece-by-piece through ManuallyDrop’s Deref/…Mut interface.

This also has the additional benefit that any accidentally overlooked panic path cannot result in doubly-frees (by skipping the mem::forget).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.