I'm just trying to experiment with Julia, so I wrote this code:
function f()
i::Int = 0
return () -> i = 1
end
@time (h = f(); sum(h() for _ in 1:1000000))
@time (h = f(); sum(h() for _ in 1:10000000))
When I ran it I obtain
0.178856 seconds (3.07 M allocations: 49.691 MiB, 4.24% gc time, 33.87% compilation time)
500000500000
1.128918 seconds (30.07 M allocations: 461.536 MiB, 4.79% gc time, 7.03% compilation time)
50000005000000
We see that it requires a lot of memory, that grows as the size of the range increases. But I don't understand why, I'd expect it to require a constant amount of memory, basically the memory to store the lambda with its captured variable (and the variables required to compute the sum). So the question is: why does the required memory grow with the size of the range?
(I know that I can obtain the same result simply with sum(1:1000000)
or with other approaches, but this is not what I am interested in, here I'm just trying to understand this specific example)
CodePudding user response:
There are two issues in your function: boxing and binding in global scope. Here is an example how both can be resolved:
julia> function f()
i = Ref(0)
return () -> i[] = 1
end
f (generic function with 1 method)
julia> @time let h = f()
sum(h() for _ in 1:1000000)
end
0.027134 seconds (113.72 k allocations: 6.053 MiB, 98.97% compilation time)
500000500000
julia> @time let h = f()
sum(h() for _ in 1:10000000)
end
0.024634 seconds (109.76 k allocations: 5.821 MiB, 96.81% compilation time)
50000005000000
julia> @time let h = f()
sum(h() for _ in 1:100000000)
end
0.031428 seconds (109.76 k allocations: 5.826 MiB, 81.02% compilation time)
5000000050000000
julia> @time let h = f()
sum(h() for _ in 1:1000000000)
end
0.088952 seconds (109.77 k allocations: 5.824 MiB, 8.99% gc time, 36.63% compilation time)
500000000500000000
CodePudding user response:
because the closure over i
is boxed, and accessing a boxed variable requires allocation, and thus it increase linearly as the number of time you access it.
The memory allocation is showing the total amount of memory allocated, not the peak memory.
and you can see the type instability:
julia> @code_warntype h()
MethodInstance for (::var"#3#4")()
from (::var"#3#4")() @ Main REPL[1]:3
Arguments
#self#::var"#3#4"
Locals
i::Union{}
Body::Int64
1 ─ %1 = Core.getfield(#self#, :i)::Core.Box
│ %2 = Core.isdefined(%1, :contents)::Bool
└── goto #3 if not %2
2 ─ goto #4
3 ─ Core.NewvarNode(:(i))
└── i
4 ┄ %7 = Core.getfield(%1, :contents)::Any
│ %8 = Core.typeassert(%7, Main.Int)::Int64
│ %9 = (%8 1)::Int64
│ = Core.getfield(#self#, :i)::Core.Box
│ = Base.convert(Main.Int, %9)::Int64
│ = Core.typeassert(, Main.Int)::Int64
│ Core.setfield!(, :contents, )
└── return %9