Hello,
I’m wondering if it’s appropriate (and necessary) to use @inbounds
on a for loop that iterates through the keys/values of a Dict
. After a quick search the examples where @inbounds
helps are where I’m indexing an Array
, for example, but I’m working with Dict
s. Here’s a test with dummy Dict
s:
k = collect(1:1:1000);
v = collect(1:2:2000);
dict = Dict(zip(k,v));
a = 0
function test1()
for v in values(dict)
a = v
end
end
function test2()
for v in values(dict)
@inbounds a = v
end
end
julia> using BenchmarkTools
julia> @benchmark test1()
BenchmarkTools.Trial:
memory estimate: 56.44 KiB
allocs estimate: 2612
--------------
minimum time: 64.799 μs (0.00% GC)
median time: 66.600 μs (0.00% GC)
mean time: 75.709 μs (1.30% GC)
maximum time: 1.054 ms (89.97% GC)
--------------
samples: 10000
evals/sample: 1
julia> @benchmark test2()
BenchmarkTools.Trial:
memory estimate: 56.44 KiB
allocs estimate: 2612
--------------
minimum time: 64.400 μs (0.00% GC)
median time: 77.900 μs (0.00% GC)
mean time: 81.345 μs (1.30% GC)
maximum time: 1.101 ms (92.41% GC)
--------------
samples: 10000
evals/sample: 1
Looks like the performance is slightly worse with @inbounds
in this case… am I missing anything?
FYI in reality the values of the dict
s I’d loop through are mutable struct
s I defined elsewhere, whose fields are mostly either Float64
or Int
.
Adding @inbounds
only affects bounds checks within the expression you’ve marked @inbounds
. In your case, that’s a = v
, which doesn’t do any indexing at all. In fact, that expression doesn’t do anything, period. The a
inside each function is a local variable, unrelated to the global variable a
with the same name, so you’re just setting the value of a local variable a
which is discarded at the end of the function.
You can, however, do @inbounds for v in ...
to also disable any bounds checking which may happen in the actual process of iterating over values.
But before doing that, there’s a much more important performance issue with this code: Both of your functions reference a non-const global variable (dict
). The slowness of iterating over a non-constant global variable dwarfs any small optimization you could get by using @inbounds
. We can fix that by making dict
const or, even better, passing it in as an argument:
julia> function test3(dict)
for v in dict
a = v
end
end
test3 (generic function with 1 method)
julia> @btime test1()
57.739 μs (2612 allocations: 56.44 KiB)
julia> @btime test2()
58.564 μs (2612 allocations: 56.44 KiB)
julia> @btime test3($dict)
7.269 μs (0 allocations: 0 bytes)
With the global variable issue fixed, we can finally see some benefit from using @inbounds
on the for loop itself:
julia> function test4(dict)
@inbounds for v in dict
a = v
end
end
test4 (generic function with 1 method)
julia> @btime test4($dict)
4.795 μs (0 allocations: 0 bytes)
For more info see Performance Tips · The Julia Language and remember to never use non-constant global variables for code whose performance you care about. After all, it’s the very first performance tip in that list for a reason.
5 Likes
Thank you very much for your explanation. You’re absolutely right: I forgot to mention that all the loops I was working with were not referencing a global dict
, but more like
function foo(dict)
...
for v in values(dict)
access/change v.field1
access/change v.field2
...
end
...
end
And I’d do dict = load_fn("filename"); foo(dict)
, where load_fn
is another function that reads a local file and build the dict
. Lastly, do I need to explicitly call foo($dict)
with $
? I thought by default Julia function arguments are passed by reference/sharing?
Thanks again 
You only need the dollar sign to get meaningful performance testing numbers, ie for @btime
For your code you should not use it (it will produce an error)
3 Likes