Strange dot-syntax result

the following is strange to me:

julia> a = [11, 12, 13, 14];
julia> a[2:4] .= view(a, 1:3);
julia> a
4-element Array{Int64,1}:
 11
 11
 12
 13

which, in my understanding, the above assignment should be equivalent to:

julia> a = [11, 12, 13, 14];

julia> for i in 1:3
           a[i + 1] = a[i]
       end

julia> a
4-element Array{Int64,1}:
 11
 11
 11
 11

that said, [11, 11, 12, 13] should be the result of a[4:-1:2].=view(a,3:-1:1), but not from a[2:4].=view(a,1:3) !

help please. thanks.

I think your assumption, that broadcasting is acting like a loop, is wrong. If this would be the case, it wouldn’t be very efficient and would be just a convenience to write shorter code, which it isn’t. You may try to benchmark it and I predict, broadcasting is faster.
This is what your assignment looks like:

 julia> Meta.@lower a[2:4] .= view(a, 1:3)
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─ %1 = 2:4
│   %2 = Base.dotview(a, %1)
│   %3 = 1:3
│   %4 = view(a, %3)
│   %5 = Base.broadcasted(Base.identity, %4)
│   %6 = Base.materialize!(%2, %5)
└──      return %6
))))

There is no loop and my guess is, that materialize! does some efficient memory moving in a single step.

how??? There’s no magic, no matter how there must be a sequence of computations.

noted that the RHS of a[2:4].=view(a,1:3) is a SubArray. That means no copy of a[1:3] is generated and being maintained. So, after the content at the address of a[2] was overwritten by that in a[1], the next operation (that puts the content pointed by a[2] into the address of a[3]) should be side-effected. I really not understand how the .= assignment works??? :dizzy_face:

Perhaps the loop is executed in reverse (i.e. with decreasing indices). In that case a[4] gets overwritten first with 13, etc.

using Debugger.@enter to check, there is indeed a loop called inside copyto!() (inside materialize!()).

Now I realize the actual problem is: using view() in manner like a[2:4].=view(a,1:3) still generates a copy and allocates memory!!!

julia> a = randn(20);

julia> @btime a[2:20] .= view(a, 1:19);
  705.296 ns (6 allocations: 416 bytes)

julia> @btime a[2:20] .= a[1:19];
  608.807 ns (4 allocations: 320 bytes)

that means using view() is actually slower and costs more memory??!!

compared to:

julia> b = randn(20);

julia> @btime a[2:20] .= view(b, 1:19);
  618.799 ns (4 allocations: 128 bytes)

julia> @btime a[2:20] .= b[1:19];
  611.046 ns (4 allocations: 320 bytes)

now, without read from and writing into the same vector, view() can save some memory.

The source and destination memory regions overlap so making a copy before executing the statement wouldn’t be unreasonable (to avoid undefined behaviour). Perhaps there is a remark on this in the docs, although I can’t immediately find it.

1 Like

Which julia version are you using? Your example works fine for me:

julia> a = [11, 12, 13, 14];

julia> a[2:4] .= view(a, 1:3)
3-element view(::Array{Int64,1}, 2:4) with eltype Int64:
 11
 12
 13

julia> a
4-element Array{Int64,1}:
 11
 11
 12
 13

julia> versioninfo()
Julia Version 1.5.2
Commit 539f3ce943 (2020-09-23 23:17 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-9.0.1 (ORCJIT, skylake)
Environment:
  JULIA_NUM_THREADS = 4

I think the issue here is what “fine” means exactly :wink: The OP was expecting certain order-dependent behaviour, but found the results to be different.

1 Like

Of course, in the end, there must be a loop, to copy memory. But it is not the equivalence to looping over the array/view elements. The lowered code implies, that the source and destination is prepared into %2(destination in array) and %5(source view) before looping and moving the data from source to destination.

And than the problems seems to be the call to Base.identity:

julia> Base.identity.(view(a, 1:3))
3-element Array{Int64,1}:
 11
 12
 13

which now is an Array and not a view anymore.

And indeed, using view uses an additional allocation:

julia> @btime a[2:4] .= a[1:3]
  323.909 ns (5 allocations: 240 bytes)

julia> @btime a[2:4] .= view(a, 1:3)
  370.588 ns (6 allocations: 352 bytes)

All in all, yes, thats interesting and I wait now for those which more indepth knowledge :wink:

This behavior is expected. Broadcasting does unaliasing before making an operation. Other than that broadcast is just a loop (but making sure to produce a correct type and shape of the output and making sure looping is efficient).

1 Like

Is this the call to Base.identity ?

For the interested, the unaliasing is happening in copyto!:

In copyto!(dest, src) at abstractarray.jl:837
 837  function copyto!(dest::AbstractArray, src::AbstractArray)
 838      isempty(src) && return dest
 839      src′ = unalias(dest, src)
>840      copyto_unaliased!(IndexStyle(dest), dest, IndexStyle(src′), src′)
 841  end

This is both expected and necessary.

this coping behavior is “too smart” in my point of view…

so, the optimal way to deal with this situation should be:

julia> a = 1.0:100.0;
julia> a_buffer = randn(99);    # pre-allocation

julia> @btime (a_buffer .= view(a, 1:99);
               a[2:100] .= a_buffer;);     # best !
  1.366 μs (6 allocations: 160 bytes)

julia> @btime a[2:100] .= view(a, 1:99);   # worst !
  883.761 ns (6 allocations: 1.05 KiB)

julia> @btime a[2:100] .= a[1:99];         # bad
  743.496 ns (4 allocations: 976 bytes)

I disagree. Syntax wise a[2:4] .= view(a, 1:3); means “put whatever is on the RHS at the time of writing this into the LHS”. Whether that is done with a loop or a memory copy of whole chunks of memory is an implementation detail. The syntax, however, is unambigous. If you want to fill your array (or a slice of it) with a specific value, use fill! (or fill!(view(a, 2:4), ...) instead.

3 Likes

Well, the alternative would be that the end result would depend on the exact order in which the copy (or other operation) is done. Which isn’t a very nice way to specify a language. It would also inhibit certain optimizations the compiler can do, which is what is happening here.

2 Likes

… using a loop.

AFAIK the traversal order of broadcasting is undefined on purpose (to allow future optimizations), so you may not want to rely on it even if it appears to work. This could change in some future version of Julia.

2 Likes

My understanding is that these points are all wrong :wink:

It is like a loop, aside from, apparently un-aliasing.

Yes, it would. Loops are fast in Julia.

I believe that’s what it is.

Without testing, it think they should be the same, or the loop would be faster.

1 Like

True. I edited above wrong statements to not confuse future readers.

1 Like