Array of Arrays times Array is type unstable

Hi all, I have a code where I need to perform a element by element multiplication of an Array of Arrays a of size X with an Array b of size X. Note that the inner arrays of a are all the same size. In other words, for every inner array of a, multiply element-wise by scalar b[x]. Using @code_warntype shows me that doing a .* b is type unstable within a larger code.

If I instead perform a two-level loop to perform the multiplication, then I don’t get a type unstable problem anymore. I can understand that maybe the compiler gets confused by the first way of writing, but then I cannot help it either because, as far as I know, you cannot pre-define the size of the inner arrays of an Array of Arrays, right?

A simple example to illustrate:

test1 = Array{Array{Float64,1},1}(undef, 10)
test2 = Array{Float64,1}(undef, 10)
for i = 1:10
    test1[i] = rand(100)
    test2[i] = rand()
end
function first_test(test1, test2)::Array{Array{Float64,1},1}
    return test1 .* test2
end
function second_test(test1, test2)::Array{Array{Float64,1},1}
    result = deepcopy(test1) # Have to deepcopy because there is no way to pre-define size of inner arrays
    for i = 1:10
        for k = 1:100
            result[i][k] = test1[i][k] * test2[i]
        end
    end
    return result
end

@code_warntype first_test(test1, test2)
@code_warntype second_test(test1, test2)

Do I have to rely on the two-level loop for these kind of operations? Is there another way to do this?

looks like this is unavoidable, maybe use a matrix for test1 instead?

1 Like

In this prototype, the dimensions of the inner arrays are the same, but in the future they could vary. Maybe a Dict would be better suited. Thanks!

Benchmarking the code provided, the type unstable version appears to be faster. It’s worth also benchmarking in the real context.

julia> @btime first_test($test1, $test2);
  1.126 μs (13 allocations: 8.97 KiB)

julia> @btime second_test($test1, $test2);
  3.853 μs (13 allocations: 9.27 KiB)

I’m surprised by the type-instability of the broadcasting operation here, but the performance of broadcasting still seems quite good. FWIW, a list comprehension works in this case as well and is type-stable (although it’s slightly slower):

julia> function first_test(test1, test2)
           return test1 .* test2
       end
first_test (generic function with 1 method)

julia> function list_comprehension(test1, test2)
           [test1[i] * test2[i] for i in eachindex(test1)]
       end
list_comprehension (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime first_test($test1, $test2);
  580.734 ns (13 allocations: 8.97 KiB)

julia> @btime list_comprehension($test1, $test2);
  604.719 ns (13 allocations: 8.97 KiB)
1 Like

Yeah, I noticed that too, but within the larger function I wrote, the type unstable version is slower, not sure why.

It is indeed, it wasn’t a huge bottleneck in the original code, I was just curious as to whether there was a better way to do this.

test1 = Array{Array{Float64,1},1}(undef, 10)
test2 = Array{Float64,1}(undef, 10)
for i = 1:10
    test1[i] = rand(100)
    test2[i] = rand()
end
function first_test(test1, test2)::Array{Array{Float64,1},1}
    return test1 .* test2
end
function second_test(test1, test2)::Array{Array{Float64,1},1}
    return [test1[i] * test2[i] for i in eachindex(test1)]
end

In my case (Julia 1.3.1 on Windows 8.1), the list comprehension version is slightly faster:

using BenchmarkTools

@btime first_test($test1, $test2);

821.094 ns (13 allocations: 8.97 KiB)

@btime second_test($test1, $test2);

734.008 ns (13 allocations: 8.97 KiB)