Tracking memory allocations
Manually
A quick way to test allocations is to use the @allocated
macro, which is available in Base
Julia. For example:
a = @allocated begin
Block to test
end; a > 0 && @show a
That will print something if the code block allocated something.
Using TimerOutputs
One practical tool is TimerOutputs. Install it as usual, and use it with, for example:
using TimerOutputs
const tmr = TimerOutput();
In the code, flag the code lines or blocks with the @timeit
macro. For example:
struct A
x
end
function test(n,x)
@timeit tmr "set y" y = Vector{A}(undef,n)
@timeit tmr "loop" for i in 1:n
@timeit tmr "assign y" y[i] = A(i*x)
end
y
end
Running this function fills the tmr
object with the time and allocation results:
julia> test(10,rand());
julia> tmr
─────────────────────────────────────────────────────────────────────
Time Allocations
────────────────────── ───────────────────────
Tot / % measured: 176s / 0.00% 103MiB / 0.00%
Section ncalls time %tot avg alloc %tot avg
─────────────────────────────────────────────────────────────────────
loop 2 8.34μs 72.6% 4.17μs 1.14KiB 78.5% 584B
assign y 20 3.18μs 27.7% 159ns 320B 21.5% 16.0B
set y 2 3.15μs 27.4% 1.58μs 320B 21.5% 160B
─────────────────────────────────────────────────────────────────────
However, @timeit
causes some allocations on its own. Therefore, nested calls can cause confusion. This can be verified in the example above. Removing the "assign y"
check inside the loop results in:
──────────────────────────────────────────────────────────────────
Time Allocations
────────────────────── ───────────────────────
Tot / % measured: 9.74s / 0.00% 6.00MiB / 0.01%
Section ncalls time %tot avg alloc %tot avg
──────────────────────────────────────────────────────────────────
set y 1 1.29μs 80.6% 1.29μs 160B 50.0% 160B
loop 1 311ns 19.4% 311ns 160B 50.0% 160B
──────────────────────────────────────────────────────────────────
Note that now the loop allocates less. Also, this result is consistent now with the Profile
result shown below.
Using the Profiler
To track allocations along the complete code, it is possible to use a profiler, although this generates so much information that it is somewhat confusing. Sometimes the output is not clear either, perhaps even wrong.
For example, consider this is the code (file name here: test.jl):
struct A
x
end
function test(n,x)
y = Vector{A}(undef,n)
for i in 1:n
y[i] = A(i*x)
end
y
end
Run julia with:
julia --track-allocation=user
Within Julia, do:
julia> using Profile
julia> include("./test.jl")
test (generic function with 1 method)
julia> test(10,rand()); # gets compiled
julia> Profile.clear_malloc_data() # clear allocations
julia> test(10,rand());
Exit Julia, this will generate a file test.jl.XXX.mem
(extension .mem
), which, in this case, contains:
- struct A
- x
- end
-
- function test(n,x)
160 y = Vector{A}(undef,n)
0 for i in 1:n
160 y[i] = A(i*x)
- end
0 y
- end
Where the lines with non-zero numbers are the lines where allocations occur.
More information: Disabling allocations