Initializing an array with literals without allocating a temporary

In this example:

buf = Int64[0, 0, 0, 0]

function foo(buf)
  buf[1] = 3
  buf[2] = 1
  buf[3] = 4
  buf[4] = 1
  nothing
end

function baz(buf)
  buf = [3, 1, 4, 1]
  nothing
end

foo doesn’t allocate, but is a bit tedious to type; baz is short, but allocates a temporary array:

julia> @btime foo($buf)
  1.841 ns (0 allocations: 0 bytes)

julia> @btime baz($buf)
  18.942 ns (1 allocation: 112 bytes)

Is there a best of both option?

SOLUTION

Macros offered by @Henrique_Becker and @rdeits are great solutions to this problem. Discourse allows selecting only one solution, so I decided to do it differently.

1 Like
julia> function baz(buf)
         buf .= (3, 1, 4, 1)
         nothing
       end
baz (generic function with 1 method)

julia> @btime baz($buf)
  5.512 ns (0 allocations: 0 bytes)

julia> @btime foo($buf)
  2.640 ns (0 allocations: 0 bytes)

Close.

5 Likes

Note that this doesn’t mutate buf, and would ideally be a no-op.

2 Likes

Good point! Old habits die so hard…

ah, sneaky, if use Array here (which is not heap allocated), it takes x5 times.

1 Like

@jling I think you meant

@pauljurczak, the version @tomerarnon wrote is the right way to do it. It is slightly slower, so if you want blazing speed (I find this hardly necessary, but I do not know your use case) you could write a macro that expands to the tedious code you write in foo, nevermind, I wrote it, it is ugly as hell, my first non-trivial macro, and will probably break if you do anything unexpected, but it is the same as writing that tedious code by hand.

julia> macro assign(vector, values)
          code = :(begin end)
          for (i, v) in enumerate(values.args)
              code = :($code; $(esc(vector))[$i] = $v)
          end
          return code
       end

julia> function foobar(buf)
           @assign buf (3, 1, 4, 1)
           nothing
       end

julia> buf = [0, 0, 0, 0];

julia> function foo(buf) # the original
         buf[1] = 3
         buf[2] = 1
         buf[3] = 4
         buf[4] = 1
         nothing
       end

julia> @code_lowered foo(buf)
CodeInfo(
1 ─     Base.setindex!(buf, 3, 1)
│       Base.setindex!(buf, 1, 2)
│       Base.setindex!(buf, 4, 3)
│       Base.setindex!(buf, 1, 4)
└──     return Main.nothing
)

julia> @code_lowered foobar(buf)
CodeInfo(
1 ─     Base.setindex!(buf, 3, 1)
│       Base.setindex!(buf, 1, 2)
│       Base.setindex!(buf, 4, 3)
│       Base.setindex!(buf, 1, 4)
└──     return Main.nothing
)

julia> @btime foo($buf)
  2.551 ns (0 allocations: 0 bytes)

julia> @btime foobar($buf)
  2.247 ns (0 allocations: 0 bytes)
4 Likes

I was just passing by but now I’m impressed by your effort

You can automate the boring way with a macro:

julia> using MacroTools

julia> macro fill(lhs, rhs)
         @capture rhs [elements__]
         quote
           begin
             @assert firstindex($(esc(lhs))) == 1 && length($(esc(lhs))) == $(length(elements))
             $([:($(esc(lhs))[$i] = $(esc(element))) for (i, element) in enumerate(elements)]...)
             $(esc(lhs))
           end
         end
       end
@fill (macro with 1 method)

Usage:

julia> x = zeros(5)
5-element Array{Float64,1}:
 0.0
 0.0
 0.0
 0.0
 0.0

julia> @fill x [1, 2, 3, 4, 5]
5-element Array{Float64,1}:
 1.0
 2.0
 3.0
 4.0
 5.0

julia> x
5-element Array{Float64,1}:
 1.0
 2.0
 3.0
 4.0
 5.0

Edit: Dang! @Henrique_Becker beat me to it :wink:

Edit2: Fixed assertion, per @pauljurczak’s suggestion.

2 Likes

XD, I have always shied away from writing macros because I already failed at it many times, but while I was writing the answer I was thinking “this really should be a macro, it really should not be that hard, I should be able to write something that, in fact it would be a good learning experience for me”, so I ended up writing it more because I decided it was the time for me to finally try to learn macros again than because I really think it is a must have.

It probably have a lot of problems in it, however. That values.args seems something that could break easily (it does not handle, for example, passing a name of a tuple variable in the local scope, it needs to be the tuple itself in the @assign line).

1 Like

Your macro is probably more robust, XD. If you can criticize mine, I would be happy to learn the details of how truly horrifying is that macro code.

Haha, no, you did fine :slightly_smiling_face:

I think you’re missing an escape on the right-hand side of each assignment. You have $v which should be $(esc(v)). You can see why this matters if you test in a function:

julia> function test!(x)
         a = 1
         @assign x [a, 2, 3, 4, 5]
       end
test! (generic function with 1 method)

julia> test!(x)
ERROR: UndefVarError: a not defined

The missing esc means you end up trying to read a variable named a in global scope, not the local one with that name.

Adding esc fixes it:

julia> macro assign(vector, values)
          code = :(begin end)
          for (i, v) in enumerate(values.args)
              code = :($code; $(esc(vector))[$i] = $(esc(v)))
          end
          return code
       end
@assign (macro with 1 method)

julia> function test!(x)
         a = 1
         @assign x [a, 2, 3, 4, 5]
       end
test! (generic function with 1 method)

julia> test!(x)
5

I agree that using .args directly is sketchy. I really like using @capture from MacroTools.jl` for this, since you can unpack exactly the syntax you expect (e.g. a list or a tuple) and get an error for any other syntax.

4 Likes

@Henrique_Becker @rdeits: I had a sneaky suspicion that a macro is the way to handle this case, but I didn’t want today to be the day I wrote the first one.

3 Likes

Your example works, but I’m getting:

julia> y = zeros(Int64, 4)
4-element Array{Int64,1}:
 0
 0
 0
 0

julia> @fill y [3, 1, 4, 1]
ERROR: AssertionError: firstindex($(Expr(:escape, [1.0, 2.0, 3.0, 4.0, 5.0]))) == 1 && length($(Expr(:escape, [1.0, 2.0, 3.0, 4.0, 5.0]))) == 4
Stacktrace:
 [1] top-level scope at /home/paul/st/julia/test.jl:728
 [2] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1088

There was a tiny error in your macro, here is corrected version:

macro fill(lhs, rhs)
  @capture rhs [elements__]
  quote
    begin
      @assert firstindex($(esc(lhs))) == 1 && length($(esc(lhs))) == $(length(elements))
      $([:($(esc(lhs))[$i] = $(esc(element))) for (i, element) in enumerate(elements)]...)
      $(esc(lhs))
    end
  end
end
3 Likes

Ahh, it escaped the x that did not exist inside the macro and just happened to work because it was the right variable name, not the lhs/rhs arguments themselves.

This kind of works but then suddenly very strange bug appears that makes me avoid writing macros when possible. Seem like my distributed and parallel processing teacher said: the problem with distributed programming is that it has all problems/bugs of non-distributed programming, plus the problems/bugs that only happen with distributed programming. The same applies to macros, :sweat_smile:.

2 Likes

Thanks! I’ve edited my post too to avoid confusing future copy-and-paste-ers.

On my machine, initializing from a tuple using @inbounds loop seems quick:

function bar(buf)
    v = (3, 1, 4, 1)
    for i in eachindex(v)
        @inbounds buf[i] = v[i]
    end
    nothing
end

Here’s the timings:

julia> @btime foo($buf)
  6.699 ns (0 allocations: 0 bytes)

julia> @btime baz($buf)
  11.010 ns (0 allocations: 0 bytes)

julia> @btime bar($buf)
  6.499 ns (0 allocations: 0 bytes)
Full code
using BenchmarkTools

buf = [0, 0, 0, 0]

function foo(buf)
    buf[1] = 3
    buf[2] = 1
    buf[3] = 4
    buf[4] = 1
    nothing
end

function baz(buf)
    buf .= (3, 1, 4, 1)
    nothing
end

function bar(buf)
    v = (3, 1, 4, 1)
    for i in eachindex(v)
        @inbounds buf[i] = v[i]
    end
    nothing
end

@btime foo($buf)
@btime baz($buf)
@btime bar($buf)
1 Like

Is buf this small in the actual use case, or is this just a simplified example?

If the actual buf is large, then storing the initialization data as code will flush the code cache of the CPU, making subsequent function calls slower.

In that case it might be better to go with something like

const ibuf = [3,1,4,1]
function bat(buf)
    buf .= ibuf
    nothing
end
1 Like

buf is small and has to be initialized at the beginning of a loop starting
an inner hot loop.

bar is distinctly the fastest on my 2 systems (execute file):

  2.184 ns (0 allocations: 0 bytes)
  5.166 ns (0 allocations: 0 bytes)
  1.642 ns (0 allocations: 0 bytes)

The timing is very unstable on my fastest PC when I measure functions one by one in REPL (fixed CPU frequency, turbo off), but the ranking is the same. I’m surprised bar is faster than foo. I even tried:

function foo(buf)
  @inbounds buf[1] = 3
  @inbounds buf[2] = 1
  @inbounds buf[3] = 4
  @inbounds buf[4] = 1
  nothing
end

and I was going to write it didn’t help, but after executing as a file with above foo, I’ve got:

  1.643 ns (0 allocations: 0 bytes)
  5.166 ns (0 allocations: 0 bytes)
  1.642 ns (0 allocations: 0 bytes)

so @inbounds macro helps in both cases.