JuliaArrays / JuliaArrays/StaticArrays.jl

Performance of using CartesianIndices with StaticArray types

Open
#1,010 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Julia
Stars
844
Forks
159
Avg merge
3d 21h
Merged PRs (30d)
3

Description

Multidimensional algorithms built on CartesianIndices seem to have significant performance overhead when applied to StaticArrays.MArray​s, but not regular Array​s. One can recover performance by writing specific functions with nested loops for each dimension, or by leveraging the macros in Base.Cartesian to automate that process.

To demonstrate, consider an N-dimensional algorithm that fills the elements of a generic input array arr using a function f(i_1, ⋯, i_N) of the indices.

function fillfn_cartesianindices!(arr::AbstractArray, f::F) where F  # force specialization
    @inbounds for I ∈ CartesianIndices(arr)
        arr[I] = f(I)
    end
    return arr
end

Here is a similar @generated function using the Base.Cartesian macros.

using Base.Cartesian

@generated function fillfn_basecartesian!(arr::AbstractArray{<:Any,N}, f::F) where {N,F}
    # Generates a function body like
    # for i_N ∈ axes(arr, N), ..., i_1 ∈ axes(arr, 1)
    #     arr[i_1, ..., i_N] = f(i_1, ..., i_N)
    # end
    quote
        @inbounds @nloops $N i arr begin
            (@nref $N arr i) = (@ncall $N f i)
        end
        return arr
    end
end

Finally, here is function of indices to feed to either of the above functions (for the four dimensional case).

indexfunc4d(i, j, k, l) = (i==k) & (j==l) - (i==l) & (j==k)
@inline indexfunc4d(I::CartesianIndex{4}) = indexfunc4d(Tuple(I)...)

For a regular Array, using CartesianIndices in the 3×3×3×3 case has no performance overhead. (In fact, it is consistently 1% to 3% faster than using explicit loops on my machine.)

using BenchmarkTools
using StaticArrays

let dims = (3,3,3,3)
    T = eltype(indexfunc4d(map(_ -> 1, dims)...))

    println("Using `CartesianIndices`")
    out1 = @btime(
        fillfn_cartesianindices!(A, $indexfunc4d),
        setup=(A = Array{$T}(undef, $dims))
    )
    println("Using `Base.Cartesian`")
    out2 = @btime(
        fillfn_basecartesian!(A, $indexfunc4d),
        setup=(A = Array{$T}(undef, $dims))
    )
    @assert out1 == out2
end;
Using `CartesianIndices`
  101.466 ns (0 allocations: 0 bytes)
Using `Base.Cartesian`
  104.874 ns (0 allocations: 0 bytes)

A similar benchmark on an MArray shows that fillfn_basecartesian is a good deal quicker than fillfn_cartesianindices.

let SDims = NTuple{4,3}
    dims = fieldtypes(SDims)
    T = eltype(indexfunc4d(map(_ -> 1, dims)...))  # Element type

    println("Using `CartesianIndices`")
    out1 = @btime(
        fillfn_cartesianindices!(A, $indexfunc4d),
        setup=(A = MArray{$SDims,$T}(undef))
    )
    println("Using `Base.Cartesian`")
    out2 = @btime(
        fillfn_basecartesian!(A, $indexfunc4d),
        setup=(A = MArray{$SDims,$T}(undef))
    )
end;
Using `CartesianIndices`
  88.504 ns (0 allocations: 0 bytes)
Using `Base.Cartesian`
  14.276 ns (0 allocations: 0 bytes)

The result is the same for a SizedArray.

let SDims = NTuple{4,3}
    dims = fieldtypes(SDims)
    T = eltype(indexfunc4d(map(_ -> 1, dims)...))  # Element type

    println("Using `CartesianIndices`")
    out1 = @btime(
        fillfn_cartesianindices!(A, $indexfunc4d),
        setup=(A = SizedArray{$SDims,$T}(undef))
    )
    println("Using `Base.Cartesian`")
    out2 = @btime(
        fillfn_basecartesian!(A, $indexfunc4d),
        setup=(A = SizedArray{$SDims,$T}(undef))
    )
end;
Using `CartesianIndices`
  87.301 ns (0 allocations: 0 bytes)
Using `Base.Cartesian`
  13.225 ns (0 allocations: 0 bytes)

Note that this problem does not apply to SArray (since it is not mutable); using StaticArrays.sacollect with CartesianIndices is efficient.

@btime StaticArrays.sacollect(SArray{NTuple{4,3}},
                              $indexfunc4d(I) for I ∈ CartesianIndices(ntuple(_ -> SOneTo(3), Val(4))));
@btime StaticArrays.sacollect(MArray{NTuple{4,3}},
                              $indexfunc4d(I) for I ∈ CartesianIndices(ntuple(_ -> SOneTo(3), Val(4))));
10.907 ns (0 allocations: 0 bytes)
51.259 ns (1 allocation: 672 bytes)

However, this way is different than trying to mutate the elements of a preallocated MArray or SizedArray.

Is there some necessary special support for CartesianIndices that is not implemented? Is it possible to implement? I need to look into exactly how CartesianIndices works in each case.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the fillfn_cartesianindices! and fillfn_basecartesian! benchmarks in the issue, comparing CartesianIndices behavior for MArray and SizedArray against regular Array. Trace how CartesianIndices and mutable StaticArrays are handled, then determine whether a focused implementation or benchmarked regression test is appropriate. Done means the performance gap is explained and any proposed support is validated against these examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.