Broadcasting assignment slow Julia

Viewed 210

I have something like this (simple example):

using BenchmarkTools
function assign()
    e = zeros(100, 90000)
    e2 = ones(100) * 0.16
    e[:, 100:end] .= e2[:]
end
@benchmark assign()

and need to this for thousands of time steps. This gives

BenchmarkTools.Trial: 
  memory estimate:  68.67 MiB
  allocs estimate:  6
  --------------
  minimum time:     16.080 ms (0.00% GC)
  median time:      27.811 ms (0.00% GC)
  mean time:        31.822 ms (12.31% GC)
  maximum time:     43.439 ms (27.66% GC)
  --------------
  samples:          158
  evals/sample:     1

Is there a faster way of doing this?

1 Answers

First of all I will assume that you meant

function assign1()
    e = zeros(100, 90000)
    e2 = ones(100) * 0.16
    e[:, 100:end] .= e2[:]
    return e  # <- important!
end

Since otherwise you will not return the first 99 columns of e(!):

julia> size(assign())
(100, 89901)

Secondly, don't do this:

e[:, 100:end] .= e2[:]

e2[:] makes a copy of e2 and assigns that, but why? Just assign e2 directly:

e[:, 100:end] .= e2

Ok, but let's try a few different versions. Notice that there is no need to make e2 a vector, just assign a scalar:

function assign2()
    e = zeros(100, 90000)
    e[:, 100:end] .= 0.16  # Just broadcast a scalar!
    return e
end

function assign3()
    e = fill(0.16, 100, 90000)  # use fill instead of writing all those zeros that you will throw away
    e[:, 1:99] .= 0
    return e
end

function assign4()
    # only write exactly the values you need!
    e = Matrix{Float64}(undef, 100, 90000)
    e[:, 1:99] .= 0
    e[:, 100:end] .= 0.16
    return e
end

Time to benchmark

julia> @btime assign1();
  14.550 ms (5 allocations: 68.67 MiB)

julia> @btime assign2();
  14.481 ms (2 allocations: 68.66 MiB)

julia> @btime assign3();
  9.636 ms (2 allocations: 68.66 MiB)

julia> @btime assign4();
  10.062 ms (2 allocations: 68.66 MiB)

Versions 1 and 2 are equally fast, but you'll notice that there are 2 allocations instead of 5, but, of course, the big allocation dominates.

Versions 3 and 4 are faster, not dramatically so, but you see that it avoids some duplicate work, such as writing values into the matrix twice. Version 3 is the fastest, not by much, but this changes if the assignment is a bit more balanced, in which case version 4 is faster:

function assign3_()
    e = fill(0.16, 100, 90000)
    e[:, 1:44999] .= 0
    return e
end

function assign4_()
    e = Matrix{Float64}(undef, 100, 90000)
    e[:, 1:44999] .= 0
    e[:, 45000:end] .= 0.16
    return e
end

julia> @btime assign3_();
  11.576 ms (2 allocations: 68.66 MiB)

julia> @btime assign4_();
  8.658 ms (2 allocations: 68.66 MiB)

The lesson is to avoid doing unnecessary work.

Related