How to update an array efficiently

Hello,

I found I use statements like this a lot:

A = func(A)

with
func a function returning a array of the same size.

Is this efficient? Or there is a better way to write this kind of things?

Thanks.

2 Likes

if type of A changed, please use let A = func(A) to update symbol for better performance.

Also, if original A is not used any more and you want a same-length and same-type array with specific initial value, use fill! and no need to assign here.

There is no need for this anymore.

It is just as effective as B = func(A). If you want to reuse the memory of A you need to update it in place, in Juila functions like that conventionally end with an exclamation mark, e.g.

function func!(A)
    # modify A
end

A = ... # initialize
func!(A)
func!(A) # reuse memory of A
5 Likes

Incredible. If it also applys when A is a free variable shared to others, I think this optimization is very advanced and cool.

1 Like

Thank you.
I use Python to do numerical computation.
Trying to learn Julia, I found some basics are quite different.
So this may be stupid question.

If I define a function like

function func!(a)
    # change a 
end

and call func!(A), then if A is an array, the values in A get changed.
But if A is just a variable, the value is not changed.

Is that right?

1 Like

First, there’s no distinction between A “is an array” vs A “is just a variable”.

If you call func!(A) and you mutate A, the change will be reflected in the A in the caller.

If you call func!(A) and you assigned to A in func!, the change will not be reflected in the caller.

This is exactly the same as python.

3 Likes

Here’s an example showing how Julia’s behavior is exactly the same as Python’s when it comes to assignment vs. mutation:

Python:

In [1]: def f(x):
   ...:     x = [1, 2, 3]
   ...:     

In [2]: x = [0, 0, 0]

In [3]: f(x)

In [4]: x
Out[4]: [0, 0, 0]

In [5]: def g(x):
   ...:     x[0] = 1
   ...:     x[1] = 2
   ...:     x[2] = 3
   ...:     

In [6]: g(x)

In [7]: x
Out[7]: [1, 2, 3]

Julia:

julia> function f(x)
         x = [1, 2, 3]
       end
f (generic function with 1 method)

julia> x = [0, 0, 0]
3-element Array{Int64,1}:
 0
 0
 0

julia> f(x)
3-element Array{Int64,1}:
 1
 2
 3

julia> x
3-element Array{Int64,1}:
 0
 0
 0                                                                                                                                                               
                                                                                                                                                                 
julia> function g(x)
         x[1] = 1
         x[2] = 2
         x[3] = 3
       end
g (generic function with 1 method)                                                                                                                               
                                                                                                                                                                 
julia> g(x)
3                                                                                                                                                                
                                                                                                                                                                 
julia> x
3-element Array{Int64,1}:
 1
 2
 3

In both languages, the function f(x) assigns a new value to the name x within the function. This has no effect on the value x which was passed in. The function g(x), on the other hand, actually mutates the value, and we can see that mutation in the value x which was passed in.

As @yuyichao said, this has nothing to do with the fact that x is an array, and is only affected by the difference between assigning a new value vs. mutating an existing value.

5 Likes

The above replies by @yuyichao and @rdeits have already given you a good explanation of what happens in each case. Here is a concrete example to answer your question directly.

julia> function func(A)
           B = sin.(A ./ 3)
       end
func (generic function with 1 method)

julia> function func!(A)
           @. A = sin(A / 3)
       end
func! (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime func($A);
  5.428 ms (2 allocations: 7.63 MiB)

julia> @btime func!($A);
  2.761 ms (0 allocations: 0 bytes)
1 Like

Thank you for all the clarification.
Now I know the difference between assignment and mutation.

Can I ask what @. A = sin(A / 3) means?

Thank you.

@. is a macro which replaces every function with an element-wise version of the function. So this replaces every element of the stay with the sin of 1/3rd of the element.

2 Likes

See More Dots: Syntactic Loop Fusion in Julia for more on @. and broadcasting.

4 Likes

Is it equivalent to A .= sin.(A ./ 3) and thus is a mutation instead of assignment?

1 Like

Yes

1 Like

If you ever wonder what a macro is doing, two approaches can help.
The first is typing ? in the repl, to enter help mode.

help?> @.
  @. expr

  Convert every function call or operator in expr into a "dot call" (e.g. convert f(x) to f.(x)), and convert every assignment in expr to a "dot assignment" (e.g. convert += to .+=).

  If you want to avoid adding dots for selected function calls in expr, splice those function calls in with $. For example, @. sqrt(abs($sort(x))) is equivalent to sqrt.(abs.(sort(x))) (no dot for sort).

  (@. is equivalent to a call to @__dot__.)

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> x = 1.0:3.0; y = similar(x);
  
  julia> @. y = x + 3 * sin(x)
  3-element Array{Float64,1}:
   3.5244129544236893
   4.727892280477045
   3.4233600241796016

This works for anything that someone wrote documentation for. That includes most things in Base Julia and many libraries (including the standard libraries). For example:

julia> using LinearAlgebra

help?> mul!
search: mul! rmul! lmul! accumulate! muladd vmul vmuladd widemul accumulate module Module mutable struct @__MODULE__ baremodule parentmodule NamedTuple isimmutable promote_rule SegmentationFault

  mul!(Y, A, B) -> Y

  Calculates the matrix-matrix or matrix-vector product AB and stores the result in Y, overwriting the existing value of Y. Note that Y must not be aliased with either A or B.

For macros specifically, there is also the helpful macro @macroexpand:

julia> @macroexpand @. A = sin(A / 3)
:(A .= sin.((/).(A, 3)))

Which expands any macros so you can see what they’re doing to the code. It’ll expand all macros in the expression that follows.

julia> @macroexpand @time @. A = sin(A / 3)
quote
    #= util.jl:158 =#
    local var"#7#stats" = Base.gc_num()
    #= util.jl:159 =#
    local var"#9#elapsedtime" = Base.time_ns()
    #= util.jl:160 =#
    local var"#8#val" = (A .= sin.((/).(A, 3)))
    #= util.jl:161 =#
    var"#9#elapsedtime" = Base.time_ns() - var"#9#elapsedtime"
    #= util.jl:162 =#
    local var"#10#diff" = Base.GC_Diff(Base.gc_num(), var"#7#stats")
    #= util.jl:163 =#
    Base.time_print(var"#9#elapsedtime", (var"#10#diff").allocd, (var"#10#diff").total_time, Base.gc_alloc_count(var"#10#diff"))
    #= util.jl:165 =#
    Base.println()
    #= util.jl:166 =#
    var"#8#val"
end

In case you find this hard to read, the library MacroTools has (among many other useful tools) the function prettify, which makes them much more readable:

julia> using MacroTools

julia> prettify(@macroexpand @time @. A = sin(A / 3))
quote
    local tapir = Base.gc_num()
    local camel = Base.time_ns()
    local guanaco = (A .= sin.((/).(A, 3)))
    camel = Base.time_ns() - camel
    local hippopotamus = Base.GC_Diff(Base.gc_num(), tapir)
    Base.time_print(camel, hippopotamus.allocd, hippopotamus.total_time, Base.gc_alloc_count(hippopotamus))
    Base.println()
    guanaco
end
13 Likes

Thanks for all replies. They are very helpful.
Julia has an excellent community.

2 Likes