This is only for benchmarking, you don’t use $ in your running code.
The time measurements are statistical samples, and subject to noise. Use @benchmark instead of @btime for a fuller picture.
This is only for benchmarking, you don’t use $ in your running code.
The time measurements are statistical samples, and subject to noise. Use @benchmark instead of @btime for a fuller picture.
You don’t have to use $ to get an accurate benchmark, you just have to avoid (untyped) global variables. Putting everything into a function is also fine:
julia> import Random.seed! as seed!
julia> function test_foreach(a)
foreach(i -> setindex!(a, rand(), i), eachindex(a));
end
julia> function test()
N = 9999
seed!(1)
a = -rand(N, N)
seed!(2)
@time @. a = rand()
seed!(1)
a = -rand(N, N)
seed!(2)
@time broadcast!(_ -> rand(), a, a)
seed!(1)
a = -rand(N, N)
seed!(2)
@time foreach(i -> setindex!(a, rand(), i), eachindex(a)) # issues with closure?
seed!(1)
a = -rand(N, N)
seed!(2)
@time test_foreach(a)
end;
julia> test() # second run
0.117221 seconds
0.111353 seconds
4.698482 seconds (199.96 M allocations: 2.980 GiB, 8.26% gc time)
0.112201 seconds
Here a function i -> setindex!(a, ...) is created.
The a is a name of a matrix, which is from the outer layer.
But the a is not written to. Why is this a closure?
What do you mean by this? a is written to by setindex!
a is mutated by setindex!, but is not re-bound to another object via a = somethingElse.
The latter leads to a Red Core.Box.
function no_written_to()
a = [1]
() -> setindex!(a, 2)
end
@code_warntype no_written_to() # a::Vector{Int64}
function has_rebind()
a = [1]
() -> setindex!(a+=1, 2)
end
@code_warntype has_rebind() # a::Core.Box
This is not a legal operation for arrays anyway:
julia> has_rebind()()
ERROR: MethodError: no method matching +(::Vector{Int64}, ::Int64)
For element-wise addition, use broadcasting with dot syntax: array .+ scalar
The function `+` exists, but no method is defined for this combination of argument types.
But even if you fix this:
function has_rebind()
a = [1]
() -> setindex!(a+=[1], 2)
end
I don’t really see what you are getting at. The above code is equivalent to
function has_rebind()
a = [1]
b = a + [1]
() -> setindex!(b, 2)
end
Edit: It seems that the compiler is better able to infer types for the latter than the former version, but semantically they are the same. Hmm, or so I think, but closures are weird. Maybe the addition happens later..
I think perhaps this is a better parallel:
function has_rebind2()
a = [1]
() -> setindex!((b = a + [1]; b), 2)
end
a is not rebound, a new array, b, is allocated and returned when calling has_rebind2()(), while a remains unaffected.
No. I think the equivalent reformulations are
function has_rebind()
a = [1]
() -> setindex!(a+=[1], 2)
end
function has_rebind()
a = [1]
function()
setindex!(a+=[1], 2)
end
end
function has_rebind()
a = [1]
function()
a = a + [1]
setindex!(a, 2)
end
end
@code_warntype has_rebind() # a::Core.Box
# cf. this
function not_has_rebind()
a = [1]
function()
a
setindex!(a, 2)
end
end
@code_warntype not_has_rebind() # a::Vector{Int}
Wait a minute, julia is AGAIN challenging my brain:
Why do the following code has Core.Box???
function very_strange()
a = [1]
a = a + [1]
() -> f(a)
end
@code_warntype very_strange() # a@_3::Core.Box, a@_4::Union{}
I’m not really getting your point, though. There is no re-binding happening in the has_rebind function. I think my latest re-formulation is the correct one:
What is f?
This is inconsequential. Because a is an input arg to f.
What f will do on a? This is irrelevant, because in the body of f, a will become another local variable within the body of f.
Purely from the standpoint of observing its appearance, There was only one a in very_strange. I cannot figure out why there are two, and even one being Core.Box. Inconceivable for me.
A concise conclusion:
function no_Core_Box()
a = 1
() -> f(a)
end
function has_Core_Box() # this is strange
a = 1
a = 1
() -> f(a)
end
function has_Core_Box() # this is clear
a = 1
function()
a = 2
end
end
This pair is also strange
function no_Core_Box()
local a
for j = 1:2
a = 1j
Threads.@spawn f(a)
end
end
function has_Core_Box()
a = 1
a = 2
Threads.@spawn f(a)
end
Compare the head line of function, let, for:
julia> function can_run(a::Bool) # can be defined
println(typeof(a))
a::Int = a
end
can_run (generic function with 1 method)
julia> can_run(true) # and can run
Bool
true
julia> function no_define() # cannot be defined
for a::Bool = true
a ::Int = a
end
end
ERROR: syntax: multiple type declarations for "a"
Stacktrace:
[1] top-level scope
@ REPL[3]:1
julia> function no_define() # cannot be defined
let a::Bool = true
a::Int = a
end
end
ERROR: syntax: multiple type declarations for "a"
Stacktrace:
[1] top-level scope
@ REPL[4]:1
It seems I have never really understood the behaviour of closures, and it seems that they in fact capture bindings, not values (contrary to my intuition, and apparently contrary to the much repeated claim that variables are “just labels”, so that mutability as a concept should not apply.)
Since closures capture variables/bindings and not values, there is a difference in how the compiler is able to optimize no_Core_Box vs has_Core_Box. With a single assignment, the compiler assumes that a = 1 is immutable, while multiple assignment confuses it. In principle, the compiler could figure out that the functions are identical, but it’s just not smart enough.
A breadcrumb (or rather, a whole bread) for you: performance of captured variables in closures · Issue #15276 · JuliaLang/julia · GitHub
In essence, closure capturing is done purely on a syntactic level at the moment (during lowering to be precise), long before anything related to type inference runs. So at the point when type inference runs, it already sees an actual call to Core.Box (inserted by lowering), and at that point it’s indistinguishable from a user-written Core.Box. This is very conservative, so any dual assignment of a captured variable defaults to boxing, causing an allocation, tanking performance, and causing all kinds of unintuitive behavior.
I probably will not have the ability to read that topic (it’s somewhat long).
But I guess one of the most annoying fact is that it destroys my intuition.
Normally if I write the code a = 789, then julia is doing “bind the label a to the object 789”.
So at any instant, a is either bound to some old object, or a is bound to a new object. In other words, there is no intermediate states. Or, It is 2-point discrete \{0, 1\}, not the continuous interval [0, 1].
But if a becomes a Core.Box inadvertently, I still write the code a = 789 from my viewpoint. But Now julia is doing mutation like
%4 = a::Core.Box
Core.setfield!(%4, :contents, 789)
i.e. Now it has a continuous possibility to be anything between the interval [0, 1]. This is annoying.
Edit: So I want to ask:
Core.Box happens during my runtime?Mutability in Julia indeed does not apply to variables because it’s a property of types and their values. Non-const variables are reassignable, and that might be called mutability in other languages where variables are named values. Same word in different contexts has different meanings.
Aren’t you detecting this with @code_warntype etc? What sort of alternative mechanism are you looking for, a linter, for example? If so, perhaps Cthulhu.jl with editor integration might be what you want.
Some times the critical part I want to look into is at some deeper level of function calling (which entails parameters that is not easy to give by hand (they were give by program when running)), e.g.
function main()
...
end
function shell()
main()
end
@code_warntype shell() # don't look into main()
Anyway, thanks.
Both Cthulhu.jl and JET.jl can do type stability checks deeper than @code_warntype.