-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Performance] iterate the smaller set during Set.intersect() #576
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
Conversation
Do you have any theory about why there's even a 1% slowdown in the average case? I should note that the implementation before your patch was suboptimal in other ways. It's really a bad idea to convert the other sequence to a Set. We should use |
@dabrahams I just ran the benchmark a few more times. It seems that 1% is within normal fluctuation between runs of the same test. Bad reporting here. |
@dabrahams My understanding is |
@dduan Regarding the 1%, that's reassuring, and what I would have expected. Regarding converting |
dfb9e14
to
cd6d28f
Compare
@dabrahams Done! |
c89da4c
to
5612d83
Compare
} | ||
} else { | ||
// sequence is ether a Set with a smaller count or not a Set at all. | ||
newSet = Set<Element>(minimumCapacity: sequence.underestimateCount()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to use min(sequence.underestimateCount(), count)
for minimumCapacity
. In that way you don't allocate too much memory for the new set, if the sequence is large and the set is small.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixing now.
104fdce
to
27f01a3
Compare
var newSet: Set<Element> | ||
|
||
if let other = sequence as? Set<Element> { | ||
newSet = Set<Element>(minimumCapacity: min(other.count, count)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if requesting capacity is the right thing here. We don't have a guarantee that the result will have that many elements. The result could even be empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. I think we should just create an empty set without requesting capacity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gribozavr the resulting size could be anywhere between 0 and min(other.count, count)
.
The closer we allocate towards min(other.count, count)
, the more we risk wasting space.
The closer we allocate towards 0, the more we risk wasting time resizing the storage.
I don't think there's a "right" way to do this. Before I make the change you suggested, just want to point out this trade off.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gribozavr @florianreinhart I ran benchmark for both arrangements and here's the result. It only represent the time aspect, but it does confirm my theory above.
newSet = Set<Element>()
small ∩ large * 100 -- 0.313429653644562
large ∩ small * 100 -- 9.32736319303513
small ∩ small * 100 -- 0.30213850736618
large ∩ large * 100 -- 19.0307229161263
total time spent ----- 28.9736542701721
newSet = Set<Element>(minimumCapacity: min(other.count, count))
small ∩ large * 100 -- 0.262056231498718
large ∩ small * 100 -- 9.16748160123825
small ∩ small * 100 -- 0.256339490413666
large ∩ large * 100 -- 15.4998977780342
total time spent ----- 25.1857751011848
2e22552
to
2c022f9
Compare
@gribozavr If memory is deemed more important than speed here, I'll make the change. Please let me know 😬 |
@dduan Thank you for the benchmarks! Swift is being used on platforms that are memory-constrained, like iOS devices, so yes, my preference is to be conservative and not over-allocate. For example, we don't reserve space in |
e1a8771
to
d7fbe51
Compare
@gribozavr Great, I've made the changes. Thank you for clarifying! |
@dduan Now you aren't iterating over the smaller collection anymore!? If Instead, I think we should do the following: If |
This introduces a small constant in speed, but it's a big win for worst case scenario.
d7fbe51
to
d919f4c
Compare
@florianreinhart hmm, restored. Thanks for catching it. I ran the benchmark and tests on Set again just as a sanity check this time.
|
Looks good to me! |
@gribozavr any other concerns? |
@dduan Looks good, thanks! |
[Performance] iterate the smaller set during Set.intersect()
XFAIL projects no longer passing after Xcode update
For Set's
intersect()
operation, iterate over the smaller of the two.This change introduces
about 1% overhead in speed for average casea small constant cost, but greatly optimizes worst case.Benchmark result:
Note: the benchmark was written for
intersectInPlace()
, which simply callsintersect()
. Tests in *validation-test/stdlib/Set.swift * passes.