Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use @inbounds annotations for Dict implementation #23843

Merged
merged 2 commits into from
Oct 2, 2017
Merged

Conversation

andyferris
Copy link
Member

@andyferris andyferris commented Sep 23, 2017

It seems to me that Dicts can be used in performance sensitive code, and ought to be as fast as possible.

I made liberty of @propagate_inbounds in order to factor out the "chain of custody" (so to speak) of the correctness of the indices.

EDIT: See microbenchmark below (I didn't see anything in BaseBenchmarks.jl...)

It seems to me that `Dict`s can be used in performance sensitive code,
and ought to be as fast as possible.

I made liberty of `@propagate_inbounds` in order to factor out the
"chain of custody" (so to speak) of the indices.
@KristofferC
Copy link
Member

Any microbenchmarks to show?

@andyferris
Copy link
Member Author

Give me one minute to recompile master. I did one on v0.6 that took 1.3 seconds and this branch takes 0.95 seconds... I'll post an update soon.

@andyferris
Copy link
Member Author

Here's a function that does some general dict-y operations. I heard BenchmakTools isn't working (didn't try) so I rolled my own:

julia> function f(n)
                  keys = 1:n
                  vals1 = 2*(1:n)
                  vals2 = 3*(1:n)
                  
                  dict = Dict{Int,Int}()
                  for k in keys
                      @inbounds dict[k] = vals1[k]
                  end
                  for k in keys
                      @inbounds dict[k] = dict[k] + vals2[k]
                  end
                  out = 0
                  for k in keys
                      out += dict[k]
                  end
                  return out
              end
f (generic function with 1 method)

julia> @noinline g(n) = f(n)
g (generic function with 1 method)

julia> h(n) = for i in 1:10000; g(n); end
h (generic function with 1 method)

On this branch (after warmup):

julia> @time h(1000)
  0.944691 seconds (180.00 k allocations: 901.795 MiB, 2.59% gc time)

julia> @time h(1000)
  0.951921 seconds (180.00 k allocations: 901.795 MiB, 2.60% gc time)

julia> @time h(1000)
  0.949090 seconds (180.00 k allocations: 901.795 MiB, 2.58% gc time)

On master

julia> @time h(1000)
  1.052432 seconds (180.00 k allocations: 901.795 MiB, 2.16% gc time)

julia> @time h(1000)
  1.051774 seconds (180.00 k allocations: 901.795 MiB, 1.85% gc time)

julia> @time h(1000)
  1.065635 seconds (180.00 k allocations: 901.795 MiB, 1.84% gc time)

On v0.6

julia> @time h(1000)
  1.296142 seconds (180.00 k allocations: 901.795 MiB, 1.35% gc time)

julia> @time h(1000)
  1.288516 seconds (180.00 k allocations: 901.795 MiB, 1.33% gc time)

julia> @time h(1000)
  1.298345 seconds (180.00 k allocations: 901.795 MiB, 1.36% gc time)

So this PR represents a 10% improvement on this benchmark (with another 25% since v0.6... nice).

base/dict.jl Outdated
@@ -713,13 +713,19 @@ function start(t::Dict)
return i
end
done(t::Dict, i) = i > length(t.vals)
next(t::Dict{K,V}, i) where {K,V} = (Pair{K,V}(t.keys[i],t.vals[i]), skip_deleted(t,i+1))
function next(t::Dict{K,V}, i) where {K,V}
@inbounds return (Pair{K,V}(t.keys[i],t.vals[i]), skip_deleted(t,i+1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is safe --- it's possible some user code could pass a bad value of i by mistake.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be marked @propagate_inbounds, though; that seems to be what arrays do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref #15291

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming that what is now here makes sense? And that for loops expand/lower to include @inbounds next(...) for fast iteration over e.g. Array?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, for loop cannot be lowered to @inbounds. It only guarantee that the iterator protocol is properly executed, but not inbounds access in next.

Copy link
Member Author

@andyferris andyferris Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm... so are you telling me that for x in vector isn't optimally fast? It really spends time checking bounds, immediately after it has verified done?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh dear, I see #15291 wasn't merged... (I figured it was merged and we've moved onto something simpler, but alas)

Copy link
Member Author

@andyferris andyferris Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yuyichao for clearing that up. At least with @propagate_inbounds here, users have the option of @inbounds for kv in dict for a speedup, right?

Also, I'll have to update my benchmark.

base/dict.jl Outdated
next(v::KeyIterator{<:Dict}, i) = (v.dict.keys[i], skip_deleted(v.dict,i+1))
next(v::ValueIterator{<:Dict}, i) = (v.dict.vals[i], skip_deleted(v.dict,i+1))
function next(v::KeyIterator{<:Dict}, i)
@inbounds return (v.dict.keys[i], skip_deleted(v.dict,i+1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@andyferris
Copy link
Member Author

Is it normal for CircleCI 64 bit to give ProcessExitedExceptions?

@fredrikekre
Copy link
Member

Is it normal for CircleCI 64 bit to give ProcessExitedExceptions?

Happens somewhat regularly.

@rfourquet
Copy link
Member

I didn't see anything in BaseBenchmarks.jl

cf. JuliaCI/BaseBenchmarks.jl#109, which benchmarks some operations with Dict, but I need some help for fixing one test before it can be merged.

@andyferris
Copy link
Member Author

OK, I'm sad we've regressed to stochastic testing... but I guess I should merge this then.

@andyferris andyferris merged commit f2fd1f8 into master Oct 2, 2017
@fredrikekre fredrikekre deleted the ajf/faster-dict branch October 2, 2017 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants