Why putting partial array in the function argument and update it actually does not change the array?

A small question.

I put the 1st column of an array in the function argument, and update its first value to 100.0.
However the array does not really change, why?

Z = [1.0 2.0; 3.0 4.0]
function f(theta::Array{Float64,1})
    theta[1] = 100.0
end
f(Z[:,1])
println(Z)

Output is
[1.0 2.0; 3.0 4.0]
So it is the same as the initial value and does not change at all.

However I expect things like
[100.0 2.0; 3.0 4.0]

Why the array does not change even when I changed it inside the function f?

I have this problem because, say I have a gigantic array Z, I only need to update part of it. So I use function like f to do the update, by inputting only some parts of the big array.
But I found it does not change the value of Z at all.

Could anyone explain it a little bit?

Thank you very much in advance!

This creates a copy of the relevant data, which is why changing it has no effect. You can use:

@view(Z[:, 1])

to get a view of that data instead of a copy. You can learn more about views here: Arrays · The Julia Language

1 Like

Thank you very much indeed! The @ view trick seems great, and it looks like view does not influence performance.

Uhm, just a naïve question, so, even for very huge array Z, do you suggest that just putting the whole Z in the argument of a function even if I just need to update partial of it? Does putting big array in the argument influence performance, it shouldn’t, right?

No, arrays are passed by reference, as in Fortran. But in Julia slices copy, thus you copied the array before passing it to the function.

It is perfectly fine to pass views if the function will operate on, or return, a slice of the original array. I would say it is even better because the function does not has to handle the dimensions that are not used.

3 Likes

Thank you very much!

A quick and stupid thing, in the above example, then how to use the view trick, I tried

 f(@view(Z[:,1]))

It gives me an error. Do I need to add a new method or definition for the theta in the argument of function f?
Or just do not define anything? Like

function f(theta)
    println(theta)
    theta[1] = 100.0
    println(theta)
end

Use AbstractVector (dependind on what the function does, and how you have written it, maybe not using anything is better. Restricting type input is important particularly if the function really does not work with other types, and sometimes for readability, but in Julia it is common to let the functions be as generic as possible).

2 Likes

From: https://github.com/JuliaLang/julia/blob/master/doc/src/manual/functions.md#argument-type-declarations

In general, you should use the most general applicable abstract types for arguments, and when in doubt, omit the argument types. You can always add argument-type specifications later if they become necessary, and you don’t sacrifice performance or functionality by omitting them.

Then

f(@view(Z[:,1]))

causes the following error:

MethodError: no method matching f(::SubArray{Float64, 1, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true})
Closest candidates are:
f(::Vector{Float64}) at …

When in doubt, omit the argument types:

function g!(theta)
    theta[1] = 100.0
end
g!(@view(Z[:,1]))
Z
2×2 Matrix{Float64}:
 100.0  2.0
   3.0  4.0

You can always add argument-type specifications later if they become necessary, and you don’t sacrifice performance or functionality by omitting them.

3 Likes

@lmiq @genkuroki
Thank you all!
Nice! So, even if I do not specify the type of the argument, it does not sacrifice the performance?
From what you said and the documents you referred to it looks like so which is cool!
Uhm, eh, is there some cases the performance may be influenced a little bit if I do not specify type?
Again, sorry if I am kind of repeating. Just wanted to repeatedly absorb some knowledge from you all.

1 Like

Not because of that.

There are situations in which you use type information inside the function, like the size of small static arrays, and in these cases one uses the fact that the size can be statically set to improve performance by guaranteeing that temporary arrays inside the function have the size determined at compile time. That is a common thing.

1 Like

Thank you!
Just a small thing, I just checked,

  1. the wrong way which copy the array,

  2. the correct @ view trick,

I checked several times, but it seems @ view is always several times slower and have more allocations and mem bytes? Am I doing wrong or something?

Use BenchmarkTools and @btime to get a more useful sense of the true time and GC

1 Like

Thank you very much!
Cool! Now the results looks much more reasonable!
The @ view trick is better.

Yes. Omitting the argument types of functions does not sacrifice the performance.

Julia beginners tend to get bugs because they carelessly write the argument types of functions.

For example, when someone defines f(x::Array) and executes f(a[:,1]) correctly, he is taught how to optimize using @view, and he rewrites it as f(@view a[:,1]), then his program will suddenly stop working.

The same can be said for f(x::String) and SubString.

When in doubt, omit the argument types.

Additional comment 1: The definition of struct should be written in such a way that the concrete types of the fields are determined, otherwise performance degradation will occur (see Performance Tips · The Julia Language). However, even in that case, too much restriction on types can cause bugs. When in doubt, you can define struct in the following way:

struct Foo{Ta, Tb, Tc}
    a::Ta
    b::Tb
    c::Tc
end

Additional comment 2: If you check the compilation process and results with @code_typed, @code_llvm, @code_native, etc., it will be more clear that the omission of argument types does not affect the performance.

Input:

f(x::Float64) = 2.0x + 1.0
@code_llvm debuginfo=:none f(1.2)

Output:

; Function Attrs: uwtable
define double @julia_f_1273(double %0) #0 {
top:
  %1 = fmul double %0, 2.000000e+00
  %2 = fadd double %1, 1.000000e+00
  ret double %2
}

Input:

g(x) = 2x + 1
@code_llvm debuginfo=:none g(1.2)

Output:

; Function Attrs: uwtable
define double @julia_g_1275(double %0) #0 {
top:
  %1 = fmul double %0, 2.000000e+00
  %2 = fadd double %1, 1.000000e+00
  ret double %2
}

The above means that the compilation results for executing f(1.2) when f(x::Float64) = 2.0x and for executing g(1.2) when g(x) = 2x are completely the same at the llvm level.

Note also that you don’t even need to write 2.0x + 1.0 because it is a floating point number calculation, 2x + 1 is enough. (See → Avoid using floats for numeric literals in generic code when possible)

3 Likes

Actually these benchmarks are not correct. With views you should not allocate anything there. It becomes more clear what is going on if you put that inside a function:

julia> function foo(x) # receives either the slice or the view
          x[1] = 1
       end
foo (generic function with 1 method)

julia> function bar1(x)
          foo(x[1:2]) #slice copy
       end
bar1 (generic function with 1 method)

julia> @btime bar1($x) # note the $, benchmarktools requires that
  27.352 ns (1 allocation: 96 bytes)
1

julia> function bar2(x)
          foo(@view(x[1:2])) #view
       end
bar2 (generic function with 1 method)

julia> @btime bar2($x)
  1.690 ns (0 allocations: 0 bytes)
1

1 Like