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 Dicts. Here’s a test with dummy Dicts:
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 dicts I’d loop through are mutable structs 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