StructArrays.jl

repository·master·Indexed 18 days ago

https://github.com/juliaarrays/structarrays.jl

Provides the `StructArray` type, which represents an array of structs using a column-major Structure-of-Arrays (SoA) internal storage format. This optimizes memory layout and performance for field-wise operations while maintaining an array-of-structs interface. It supports custom data layouts, CUDA kernel integration via `replace_storage`, lazy row iteration with `LazyRow` and `LazyRows`, and compatibility with StaticArrays.jl.

Tokens
4.6K
Snippets
16
Records
25
Agent score
64%

What's inside StructArrays.jl

  1. What is StructArrays.jl?

    master
    StructArrays.jl provides the StructArray type, which allows you to treat a collection of struct elements as a single array. While it behaves like an array of structs, it is internally stored as a list of arrays (typically one array per field of the struct). This representation is more memory-efficient and provides better performance for many array operations compared to a standard array of structs.
  2. What is a StructArray and how does it work?

    master

    A StructArray is an AbstractArray where elements are treated as structs (like NamedTuples, ComplexF64, or custom user-defined structs), but the underlying data is stored using a Structure-Of-Arrays (SOA) layout.

    Instead of storing individual structs in a single array (Array-Of-Structs or AOS), StructArray maintains separate arrays for each field of the struct. The struct entries are constructed on-the-fly when accessed. This allows for efficient field-wise operations while providing a high-level interface where you can access fields using dot syntax or getproperty.

  3. Understand the difference between SOA views and AOS copies

    master

    How StructArray behaves depends on how it is initialized:

    1. SOA (Structure of Arrays) View: When you create a StructArray from separate parent arrays (e.g., StructArray((a, b))), it creates a view. Modifying the StructArray (via field access like soa.a[i] or element assignment soa[i] = ...) will modify the original parent arrays. However, modifying a field of a materialized struct element (e.g., soa[i].field = val) is transient and will not be saved.

    2. AOS (Array of Structs) Copy: When you create a StructArray from an existing array of structs (e.g., StructArray(aos)), it creates a copy. The StructArray is decoupled from the original array; changes to the StructArray will not affect the original aos array.

    # SOA View: Modifying soa modifies a and b
    a = [1, 1]; b = [2, 2]
    soa = StructArray{Foo}((a, b))
    soa.a[1] = 5  # 'a' is now [5, 1]
    
    # AOS Copy: Modifying soa does NOT modify aos
    aos = [Foo(1,2), Foo(1,2)]
    soa = StructArray(aos)
    soa.a[1] = 5  # 'aos' remains unchanged
  4. Best practice: Use immutable structs with StructArray

    master

    It is highly recommended to use immutable structs with StructArray whenever possible.

    • Safety: With immutable structs, attempting to modify a field of a materialized element (e.g., soa[i].field = val) will result in a compile-time error rather than silent, transient behavior.
    • Performance: The performance of immutable struct creation is generally much better than for mutable structs.
  5. Implement custom data layouts for StructArrays

    master

    You can support non-standard data layouts in StructArray by overloading three specific methods for your type T. This allows StructArray to unpack nested or complex structures (like NamedTuples inside a struct) into individual fields.

    To implement a custom layout, you must provide:

    1. StructArrays.staticschema(::Type{T}): Defines the names and element types of the fields that StructArray should expose.
    2. StructArrays.component(m::T, key::Symbol): A component-extractor that retrieves a specific field from an instance m given a key.
    3. StructArrays.createinstance(::Type{T}, x, args...): A constructor-like method that recreates an instance of T from its constituent components.

    An implementation is successful if createinstance(T, (component(x, f) for f in fieldnames(staticschema(T)))...) returns a valid instance of T.

    # Example: Unpacking a NamedTuple field into top-level StructArray fields
    struct MyType{T, NT<:NamedTuple}
        data::T
        rest::NT
    end
    
    # 1. Define schema
    function StructArrays.staticschema(::Type{MyType{T, NamedTuple{names, types}}}) where {T, names, types}
        return NamedTuple{(:data, names...), Base.tuple_type_cons(T, types)}
    end;
    
    # 2. Define extractor
    function StructArrays.component(m::MyType, key::Symbol)
        return key === :data ? getfield(m, 1) : getfield(getfield(m, 2), key)
    end;
    
    # 3. Define re-constructor
    function StructArrays.createinstance(::Type{MyType{T, NT}}, x, args...) where {T, NT}
        return MyType(x, NT(args))
    end;
  6. How to persistently modify a field of a mutable struct element

    master

    Because StructArray (in SOA mode) materializes struct elements on-the-fly, modifying a field of a retrieved element (e.g., soa[i].field = val) only modifies a temporary object that is immediately discarded.

    To persist a change to a field in a mutable struct, you must retrieve the element, modify it, and then assign it back to the StructArray index.

    # INCORRECT: This change is lost
    soa[4].b = 9
    
    # CORRECT: Retrieve, modify, and re-assign
    x = soa[4]
    x.b = 10
    soa[4] = x
  7. Use StructArrays in CUDA kernels

    master

    You can use StructArrays directly within CUDA kernels by moving the storage to the GPU. To do this, use replace_storage(CuArray, d) where d is your StructArray.

    Workflow:

    1. Create your StructArray on the CPU.
    2. Use replace_storage(CuArray, d) to create a GPU-resident version (dd).
    3. Create a destination array on the GPU using similar(dd).
    4. Launch a standard CUDA kernel that operates on these arrays.
    using CUDA, StructArrays
    
    d = StructArray(a = rand(100), b = rand(100))
    
    # move to GPU
    dd = replace_storage(CuArray, d)
    de = similar(dd)
    
    # A simple kernel to copy content
    function kernel!(dest, src)
        i = (blockIdx().x-1)*blockDim().x + threadIdx().x
        if i <= length(dest)
            dest[i] = src[i]
        end
        return nothing
    end
    
    threads = 1024
    blocks = cld(length(dd), threads)
    
    @cuda threads=threads blocks=blocks kernel!(de, dd)
  8. Avoid broadcasted in-place assignment for StructArray entries

    master

    Using broadcasted in-place assignment (e.g., x[i] .= val) on a StructArray entry may not work as expected. This is because the broadcast operation creates a new materialized struct first, and the in-place modification applies to that temporary object rather than the underlying storage of the StructArray.

    To ensure the change is saved, assign the result of the broadcast back to the index: x[i] = x[i] .= val.

    # This might fail to update the StructArray storage:
    x[1] .= 1
    
    # This works:
    x[1] = x[1] .= 1
  9. Store complex numbers in a StructArray

    master

    You can use StructArray to decompose complex numbers into their real (re) and imaginary (im) components. This can be done by specifying the target type ComplexF64 and providing a tuple of arrays representing the components, or by passing a standard array of complex numbers directly to the StructArray constructor.

    Use StructArrays.components(s) to obtain all field arrays as a named tuple.

    using StructArrays, Random
    
    # Method 1: Decomposing components manually
    Random.seed!(4);
    s = StructArray{ComplexF64}((rand(2,2), rand(2,2)))
    s.re # Access real part
    s.im # Access imaginary part
    StructArrays.components(s) # Returns (re = ..., im = ...)
    
    # Method 2: Converting an existing array of complex numbers
    StructArray([1+im, 3-2im])
  10. Use StructArray with StaticArray elements

    master

    StructArray is compatible with StaticArrays.jl types like SVector, SMatrix, and SArray. This allows you to create arrays of small, fixed-size structures that benefit from the performance of static dispatch while maintaining the StructArray interface.

    using StructArrays, StaticArrays
    
    # Array of SVector
    x = StructArray([SVector{2}(1,2) for i = 1:5])
    
    # Array of SMatrix
    A = StructArray([SMatrix{2,2}([1 2;3 4]) for i = 1:5])
    
    # Array of higher-dimensional SArray
    B = StructArray([SArray{Tuple{2,2,2}}(reshape(1:8,2,2,2)) for i = 1:5])
    B[1]
  11. Store a data table using StructArray

    master

    A StructArray can act as a data table by taking a collection of named tuples or a tuple of arrays with matching names. Each element in the StructArray represents a row (a named tuple), and you can access entire columns using dot notation (e.g., t.a).

    You can grow the table using push! with new named tuples.

    # Create a table from named fields
    t = StructArray((a = [1, 2], b = ["x", "y"]))
    
    # Access a row
    t[1] # (a = 1, b = "x")
    
    # Access a column
    t.a # [1, 2]
    
    # Add a new row
    push!(t, (a = 3, b = "z"))