OffsetArrays vs Arrays speed

Hello Julia wizards!

I’ve been exploring this language for about a week, and so far I’m very very intrigued. I have a simulation code I’m writing that would greatly benefit from using OffsetArrays, but everything I try results in a massive slowdown.

I’ve boiled down the pattern to a simple example, and all of the documentation I can understand suggests that the Array version and the OffsetArray version should be the same speed. The benchmark result is:

Arrays: 730.958 μs (0 allocations: 0 bytes)
OffsetArrays: 129.026 ms (9998470 allocations: 167.82 MiB)

My guess is all of those unexpected allocations are the problem, but I have no idea where they are coming from. Help, please?

using BenchmarkTools
using OffsetArrays

mutable struct MyArray
    a::Array{Float64, 1}
    b::Array{Float64, 1}

    MyArray() = new()
    MyArray(a, b) = new(a,b)
end

mutable struct MyOffsetArray
    a::OffsetArray{Float64, 1}
    b::OffsetArray{Float64, 1}

    MyOffsetArray() = new()
    MyOffsetArray(a, b) = new(a,b)
end

function myoperate(arg::Union{MyArray, MyOffsetArray})
    for i = 1:length(arg.b)
        arg.b[i] = arg.b[i] + 0.5 * (arg.a[i] + arg.a[i+1])
    end
end

function timeit()
    nx = 1000000

    a = rand(nx+1)
    b = rand(nx)

    array = MyArray(a, b)
    offset = MyOffsetArray(a, b)

    @btime myoperate($array)
    @btime myoperate($offset)
end

timeit()

The problem is that OffsetArray{Float64, 1} is an abstract type (since the offset is also part of the type). You probably want to use

mutable struct MyParametricArray{T<:AbstractArray}
    a::T
    b::T
    MyParametricArray() = new()
    MyParametricArray(a, b) = new(a,b)
end

which will specialize correctly.

7 Likes

Wow, thanks! That led me the right way (and it would have been a long time before I figured that out…)

Arrays: 734.166 μs (0 allocations: 0 bytes)
OffsetArrays: 128.470 ms (9998470 allocations: 167.82 MiB)
OffsetArrays (parametric): 1.276 ms (0 allocations: 0 bytes)

Final code:

using BenchmarkTools
using OffsetArrays

mutable struct MyArray
    a::Array{Float64, 1}
    b::Array{Float64, 1}
end

mutable struct MyOffsetArray
    a::OffsetArray{Float64, 1}
    b::OffsetArray{Float64, 1}
end

mutable struct MyParametricArray{T<:AbstractArray}
    a::T
    b::T
end

function myoperate(arg::Union{MyArray, MyOffsetArray, MyParametricArray})
    for i = 1:length(arg.b)
        arg.b[i] = arg.b[i] + 0.5 * (arg.a[i] + arg.a[i+1])
    end
end

function timeit()
    nx = 1000000

    a = rand(nx+1)
    b = rand(nx)

    ao = OffsetArrays.Origin(1)(a)
    bo = OffsetArrays.Origin(1)(b)

    array = MyArray(a, b)
    offset = MyOffsetArray(ao, bo)
    parameter = MyParametricArray(ao, bo)

    @btime myoperate($array)
    @btime myoperate($offset)
    @btime myoperate($parameter)
end

timeit()

This seems highly risky. If a and b are OffsetArrays, those will likely not be valid indices (that’s the point of OffsetArrays, really). Use eachindex(a, b) instead.

Also, consider whether you need your type to be mutable. Immutable types are generally more efficient, and you can still mutate mutable fields of an immutable type, so your array code would still work.

6 Likes

Yes, I use eachindex in the real code. I was just trying to boil the problem down to the minimal case here.

Thanks for the tip about mutable.