Call FastClosures.jl `@closure` in Array Comprehensions?

Like with any closure, array comprehensions present a performance challenge if they capture a variable whose identifier is reassigned:

julia> using BenchmarkTools

julia> const a=[1:1000;];

julia> @btime let b=a; [b[x] for x ∈ b] end;
  436.224 ns (1 allocation: 7.94 KiB)

julia> @btime let b=a; b=b; [b[x] for x ∈ b] end;
  23.000 μs (984 allocations: 23.33 KiB)

julia> @btime let b=a; c=b; [c[x] for x ∈ c] end;
  412.366 ns (1 allocation: 7.94 KiB)

Unlike generators, comprehensions offer syntactical assurance that the closure will be executed and consumed immediately—so there’s no reason to box the captures. Additionally, the use of a lambda is just an implementation detail, which adds surprise when someone new to Julia is confronted with this performance degradation.

Would it then make sense for Julia to use the FastClosures.jl @closure macro to prevent boxing here, as recommended in the performance manual?

You possibly meant to put a comma here, instead of a semicolon (a semicolon is like a newline, which is syntactically significant for let). Doing that makes the timing match the other two timings.

No, this is intentional. The intent was to contrive a scenario where neophytes to Julia (and even seasoned veterans) could inadvertently cause boxing and poor performance where they didn’t expect to.

This is a better example, using more common patterns:

julia> function test1()
           x = if true;  collect(1:1000)  else  collect(1:1000)  end
           [x[i] for i in x]
       end;

julia> function test2()
           if true;  x=collect(1:1000)  else  x=collect(1:1000)  end
           [x[i] for i in x]
       end;

julia> @btime test1();
  725.197 ns (2 allocations: 15.88 KiB)

julia> @btime test2();
  23.600 μs (985 allocations: 31.27 KiB)

Even for people who’ve read #15276, it might not be anticipated that a box would be formed here, because the fact that a comprehension creates a lambda is not obvious or necessary.

2 Likes