1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#ifndef ART_RUNTIME_BASE_HASH_SET_H_
18#define ART_RUNTIME_BASE_HASH_SET_H_
19
20#include <functional>
21#include <iterator>
22#include <memory>
23#include <stdint.h>
24#include <utility>
25
26#include "bit_utils.h"
27#include "logging.h"
28
29namespace art {
30
31// Returns true if an item is empty.
32template <class T>
33class DefaultEmptyFn {
34 public:
35  void MakeEmpty(T& item) const {
36    item = T();
37  }
38  bool IsEmpty(const T& item) const {
39    return item == T();
40  }
41};
42
43template <class T>
44class DefaultEmptyFn<T*> {
45 public:
46  void MakeEmpty(T*& item) const {
47    item = nullptr;
48  }
49  bool IsEmpty(T* const& item) const {
50    return item == nullptr;
51  }
52};
53
54// Low memory version of a hash set, uses less memory than std::unordered_set since elements aren't
55// boxed. Uses linear probing to resolve collisions.
56// EmptyFn needs to implement two functions MakeEmpty(T& item) and IsEmpty(const T& item).
57// TODO: We could get rid of this requirement by using a bitmap, though maybe this would be slower
58// and more complicated.
59template <class T, class EmptyFn = DefaultEmptyFn<T>, class HashFn = std::hash<T>,
60    class Pred = std::equal_to<T>, class Alloc = std::allocator<T>>
61class HashSet {
62  template <class Elem, class HashSetType>
63  class BaseIterator : std::iterator<std::forward_iterator_tag, Elem> {
64   public:
65    BaseIterator(const BaseIterator&) = default;
66    BaseIterator(BaseIterator&&) = default;
67    BaseIterator(HashSetType* hash_set, size_t index) : index_(index), hash_set_(hash_set) {
68    }
69    BaseIterator& operator=(const BaseIterator&) = default;
70    BaseIterator& operator=(BaseIterator&&) = default;
71
72    bool operator==(const BaseIterator& other) const {
73      return hash_set_ == other.hash_set_ && this->index_ == other.index_;
74    }
75
76    bool operator!=(const BaseIterator& other) const {
77      return !(*this == other);
78    }
79
80    BaseIterator operator++() {  // Value after modification.
81      this->index_ = this->NextNonEmptySlot(this->index_, hash_set_);
82      return *this;
83    }
84
85    BaseIterator operator++(int) {
86      BaseIterator temp = *this;
87      this->index_ = this->NextNonEmptySlot(this->index_, hash_set_);
88      return temp;
89    }
90
91    Elem& operator*() const {
92      DCHECK(!hash_set_->IsFreeSlot(this->index_));
93      return hash_set_->ElementForIndex(this->index_);
94    }
95
96    Elem* operator->() const {
97      return &**this;
98    }
99
100    // TODO: Operator -- --(int)  (and use std::bidirectional_iterator_tag)
101
102   private:
103    size_t index_;
104    HashSetType* hash_set_;
105
106    size_t NextNonEmptySlot(size_t index, const HashSet* hash_set) const {
107      const size_t num_buckets = hash_set->NumBuckets();
108      DCHECK_LT(index, num_buckets);
109      do {
110        ++index;
111      } while (index < num_buckets && hash_set->IsFreeSlot(index));
112      return index;
113    }
114
115    friend class HashSet;
116  };
117
118 public:
119  using value_type = T;
120  using allocator_type = Alloc;
121  using reference = T&;
122  using const_reference = const T&;
123  using pointer = T*;
124  using const_pointer = const T*;
125  using iterator = BaseIterator<T, HashSet>;
126  using const_iterator = BaseIterator<const T, const HashSet>;
127  using size_type = size_t;
128  using difference_type = ptrdiff_t;
129
130  static constexpr double kDefaultMinLoadFactor = 0.4;
131  static constexpr double kDefaultMaxLoadFactor = 0.7;
132  static constexpr size_t kMinBuckets = 1000;
133
134  // If we don't own the data, this will create a new array which owns the data.
135  void Clear() {
136    DeallocateStorage();
137    num_elements_ = 0;
138    elements_until_expand_ = 0;
139  }
140
141  HashSet() : HashSet(kDefaultMinLoadFactor, kDefaultMaxLoadFactor) {}
142
143  HashSet(double min_load_factor, double max_load_factor) noexcept
144      : num_elements_(0u),
145        num_buckets_(0u),
146        elements_until_expand_(0u),
147        owns_data_(false),
148        data_(nullptr),
149        min_load_factor_(min_load_factor),
150        max_load_factor_(max_load_factor) {
151    DCHECK_GT(min_load_factor, 0.0);
152    DCHECK_LT(max_load_factor, 1.0);
153  }
154
155  explicit HashSet(const allocator_type& alloc) noexcept
156      : allocfn_(alloc),
157        hashfn_(),
158        emptyfn_(),
159        pred_(),
160        num_elements_(0u),
161        num_buckets_(0u),
162        elements_until_expand_(0u),
163        owns_data_(false),
164        data_(nullptr),
165        min_load_factor_(kDefaultMinLoadFactor),
166        max_load_factor_(kDefaultMaxLoadFactor) {
167  }
168
169  HashSet(const HashSet& other) noexcept
170      : allocfn_(other.allocfn_),
171        hashfn_(other.hashfn_),
172        emptyfn_(other.emptyfn_),
173        pred_(other.pred_),
174        num_elements_(other.num_elements_),
175        num_buckets_(0),
176        elements_until_expand_(other.elements_until_expand_),
177        owns_data_(false),
178        data_(nullptr),
179        min_load_factor_(other.min_load_factor_),
180        max_load_factor_(other.max_load_factor_) {
181    AllocateStorage(other.NumBuckets());
182    for (size_t i = 0; i < num_buckets_; ++i) {
183      ElementForIndex(i) = other.data_[i];
184    }
185  }
186
187  // noexcept required so that the move constructor is used instead of copy constructor.
188  // b/27860101
189  HashSet(HashSet&& other) noexcept
190      : allocfn_(std::move(other.allocfn_)),
191        hashfn_(std::move(other.hashfn_)),
192        emptyfn_(std::move(other.emptyfn_)),
193        pred_(std::move(other.pred_)),
194        num_elements_(other.num_elements_),
195        num_buckets_(other.num_buckets_),
196        elements_until_expand_(other.elements_until_expand_),
197        owns_data_(other.owns_data_),
198        data_(other.data_),
199        min_load_factor_(other.min_load_factor_),
200        max_load_factor_(other.max_load_factor_) {
201    other.num_elements_ = 0u;
202    other.num_buckets_ = 0u;
203    other.elements_until_expand_ = 0u;
204    other.owns_data_ = false;
205    other.data_ = nullptr;
206  }
207
208  // Construct from existing data.
209  // Read from a block of memory, if make_copy_of_data is false, then data_ points to within the
210  // passed in ptr_.
211  HashSet(const uint8_t* ptr, bool make_copy_of_data, size_t* read_count) noexcept {
212    uint64_t temp;
213    size_t offset = 0;
214    offset = ReadFromBytes(ptr, offset, &temp);
215    num_elements_ = static_cast<uint64_t>(temp);
216    offset = ReadFromBytes(ptr, offset, &temp);
217    num_buckets_ = static_cast<uint64_t>(temp);
218    CHECK_LE(num_elements_, num_buckets_);
219    offset = ReadFromBytes(ptr, offset, &temp);
220    elements_until_expand_ = static_cast<uint64_t>(temp);
221    offset = ReadFromBytes(ptr, offset, &min_load_factor_);
222    offset = ReadFromBytes(ptr, offset, &max_load_factor_);
223    if (!make_copy_of_data) {
224      owns_data_ = false;
225      data_ = const_cast<T*>(reinterpret_cast<const T*>(ptr + offset));
226      offset += sizeof(*data_) * num_buckets_;
227    } else {
228      AllocateStorage(num_buckets_);
229      // Write elements, not that this may not be safe for cross compilation if the elements are
230      // pointer sized.
231      for (size_t i = 0; i < num_buckets_; ++i) {
232        offset = ReadFromBytes(ptr, offset, &data_[i]);
233      }
234    }
235    // Caller responsible for aligning.
236    *read_count = offset;
237  }
238
239  // Returns how large the table is after being written. If target is null, then no writing happens
240  // but the size is still returned. Target must be 8 byte aligned.
241  size_t WriteToMemory(uint8_t* ptr) const {
242    size_t offset = 0;
243    offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(num_elements_));
244    offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(num_buckets_));
245    offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(elements_until_expand_));
246    offset = WriteToBytes(ptr, offset, min_load_factor_);
247    offset = WriteToBytes(ptr, offset, max_load_factor_);
248    // Write elements, not that this may not be safe for cross compilation if the elements are
249    // pointer sized.
250    for (size_t i = 0; i < num_buckets_; ++i) {
251      offset = WriteToBytes(ptr, offset, data_[i]);
252    }
253    // Caller responsible for aligning.
254    return offset;
255  }
256
257  ~HashSet() {
258    DeallocateStorage();
259  }
260
261  HashSet& operator=(HashSet&& other) noexcept {
262    HashSet(std::move(other)).swap(*this);
263    return *this;
264  }
265
266  HashSet& operator=(const HashSet& other) noexcept {
267    HashSet(other).swap(*this);  // NOLINT(runtime/explicit) - a case of lint gone mad.
268    return *this;
269  }
270
271  // Lower case for c++11 for each.
272  iterator begin() {
273    iterator ret(this, 0);
274    if (num_buckets_ != 0 && IsFreeSlot(ret.index_)) {
275      ++ret;  // Skip all the empty slots.
276    }
277    return ret;
278  }
279
280  // Lower case for c++11 for each. const version.
281  const_iterator begin() const {
282    const_iterator ret(this, 0);
283    if (num_buckets_ != 0 && IsFreeSlot(ret.index_)) {
284      ++ret;  // Skip all the empty slots.
285    }
286    return ret;
287  }
288
289  // Lower case for c++11 for each.
290  iterator end() {
291    return iterator(this, NumBuckets());
292  }
293
294  // Lower case for c++11 for each. const version.
295  const_iterator end() const {
296    return const_iterator(this, NumBuckets());
297  }
298
299  bool Empty() {
300    return Size() == 0;
301  }
302
303  // Return true if the hash set has ownership of the underlying data.
304  bool OwnsData() const {
305    return owns_data_;
306  }
307
308  // Erase algorithm:
309  // Make an empty slot where the iterator is pointing.
310  // Scan forwards until we hit another empty slot.
311  // If an element in between doesn't rehash to the range from the current empty slot to the
312  // iterator. It must be before the empty slot, in that case we can move it to the empty slot
313  // and set the empty slot to be the location we just moved from.
314  // Relies on maintaining the invariant that there's no empty slots from the 'ideal' index of an
315  // element to its actual location/index.
316  iterator Erase(iterator it) {
317    // empty_index is the index that will become empty.
318    size_t empty_index = it.index_;
319    DCHECK(!IsFreeSlot(empty_index));
320    size_t next_index = empty_index;
321    bool filled = false;  // True if we filled the empty index.
322    while (true) {
323      next_index = NextIndex(next_index);
324      T& next_element = ElementForIndex(next_index);
325      // If the next element is empty, we are done. Make sure to clear the current empty index.
326      if (emptyfn_.IsEmpty(next_element)) {
327        emptyfn_.MakeEmpty(ElementForIndex(empty_index));
328        break;
329      }
330      // Otherwise try to see if the next element can fill the current empty index.
331      const size_t next_hash = hashfn_(next_element);
332      // Calculate the ideal index, if it is within empty_index + 1 to next_index then there is
333      // nothing we can do.
334      size_t next_ideal_index = IndexForHash(next_hash);
335      // Loop around if needed for our check.
336      size_t unwrapped_next_index = next_index;
337      if (unwrapped_next_index < empty_index) {
338        unwrapped_next_index += NumBuckets();
339      }
340      // Loop around if needed for our check.
341      size_t unwrapped_next_ideal_index = next_ideal_index;
342      if (unwrapped_next_ideal_index < empty_index) {
343        unwrapped_next_ideal_index += NumBuckets();
344      }
345      if (unwrapped_next_ideal_index <= empty_index ||
346          unwrapped_next_ideal_index > unwrapped_next_index) {
347        // If the target index isn't within our current range it must have been probed from before
348        // the empty index.
349        ElementForIndex(empty_index) = std::move(next_element);
350        filled = true;  // TODO: Optimize
351        empty_index = next_index;
352      }
353    }
354    --num_elements_;
355    // If we didn't fill the slot then we need go to the next non free slot.
356    if (!filled) {
357      ++it;
358    }
359    return it;
360  }
361
362  // Find an element, returns end() if not found.
363  // Allows custom key (K) types, example of when this is useful:
364  // Set of Class* sorted by name, want to find a class with a name but can't allocate a dummy
365  // object in the heap for performance solution.
366  template <typename K>
367  iterator Find(const K& key) {
368    return FindWithHash(key, hashfn_(key));
369  }
370
371  template <typename K>
372  const_iterator Find(const K& key) const {
373    return FindWithHash(key, hashfn_(key));
374  }
375
376  template <typename K>
377  iterator FindWithHash(const K& key, size_t hash) {
378    return iterator(this, FindIndex(key, hash));
379  }
380
381  template <typename K>
382  const_iterator FindWithHash(const K& key, size_t hash) const {
383    return const_iterator(this, FindIndex(key, hash));
384  }
385
386  // Insert an element, allows duplicates.
387  void Insert(const T& element) {
388    InsertWithHash(element, hashfn_(element));
389  }
390
391  void InsertWithHash(const T& element, size_t hash) {
392    DCHECK_EQ(hash, hashfn_(element));
393    if (num_elements_ >= elements_until_expand_) {
394      Expand();
395      DCHECK_LT(num_elements_, elements_until_expand_);
396    }
397    const size_t index = FirstAvailableSlot(IndexForHash(hash));
398    data_[index] = element;
399    ++num_elements_;
400  }
401
402  size_t Size() const {
403    return num_elements_;
404  }
405
406  void swap(HashSet& other) {
407    // Use argument-dependent lookup with fall-back to std::swap() for function objects.
408    using std::swap;
409    swap(allocfn_, other.allocfn_);
410    swap(hashfn_, other.hashfn_);
411    swap(emptyfn_, other.emptyfn_);
412    swap(pred_, other.pred_);
413    std::swap(data_, other.data_);
414    std::swap(num_buckets_, other.num_buckets_);
415    std::swap(num_elements_, other.num_elements_);
416    std::swap(elements_until_expand_, other.elements_until_expand_);
417    std::swap(min_load_factor_, other.min_load_factor_);
418    std::swap(max_load_factor_, other.max_load_factor_);
419    std::swap(owns_data_, other.owns_data_);
420  }
421
422  allocator_type get_allocator() const {
423    return allocfn_;
424  }
425
426  void ShrinkToMaximumLoad() {
427    Resize(Size() / max_load_factor_);
428  }
429
430  // Reserve enough room to insert until Size() == num_elements without requiring to grow the hash
431  // set. No-op if the hash set is already large enough to do this.
432  void Reserve(size_t num_elements) {
433    size_t num_buckets = num_elements / max_load_factor_;
434    // Deal with rounding errors. Add one for rounding.
435    while (static_cast<size_t>(num_buckets * max_load_factor_) <= num_elements + 1u) {
436      ++num_buckets;
437    }
438    if (num_buckets > NumBuckets()) {
439      Resize(num_buckets);
440    }
441  }
442
443  // To distance that inserted elements were probed. Used for measuring how good hash functions
444  // are.
445  size_t TotalProbeDistance() const {
446    size_t total = 0;
447    for (size_t i = 0; i < NumBuckets(); ++i) {
448      const T& element = ElementForIndex(i);
449      if (!emptyfn_.IsEmpty(element)) {
450        size_t ideal_location = IndexForHash(hashfn_(element));
451        if (ideal_location > i) {
452          total += i + NumBuckets() - ideal_location;
453        } else {
454          total += i - ideal_location;
455        }
456      }
457    }
458    return total;
459  }
460
461  // Calculate the current load factor and return it.
462  double CalculateLoadFactor() const {
463    return static_cast<double>(Size()) / static_cast<double>(NumBuckets());
464  }
465
466  // Make sure that everything reinserts in the right spot. Returns the number of errors.
467  size_t Verify() NO_THREAD_SAFETY_ANALYSIS {
468    size_t errors = 0;
469    for (size_t i = 0; i < num_buckets_; ++i) {
470      T& element = data_[i];
471      if (!emptyfn_.IsEmpty(element)) {
472        T temp;
473        emptyfn_.MakeEmpty(temp);
474        std::swap(temp, element);
475        size_t first_slot = FirstAvailableSlot(IndexForHash(hashfn_(temp)));
476        if (i != first_slot) {
477          LOG(ERROR) << "Element " << i << " should be in slot " << first_slot;
478          ++errors;
479        }
480        std::swap(temp, element);
481      }
482    }
483    return errors;
484  }
485
486  double GetMinLoadFactor() const {
487    return min_load_factor_;
488  }
489
490  double GetMaxLoadFactor() const {
491    return max_load_factor_;
492  }
493
494  // Change the load factor of the hash set. If the current load factor is greater than the max
495  // specified, then we resize the hash table storage.
496  void SetLoadFactor(double min_load_factor, double max_load_factor) {
497    DCHECK_LT(min_load_factor, max_load_factor);
498    DCHECK_GT(min_load_factor, 0.0);
499    DCHECK_LT(max_load_factor, 1.0);
500    min_load_factor_ = min_load_factor;
501    max_load_factor_ = max_load_factor;
502    elements_until_expand_ = NumBuckets() * max_load_factor_;
503    // If the current load factor isn't in the range, then resize to the mean of the minimum and
504    // maximum load factor.
505    const double load_factor = CalculateLoadFactor();
506    if (load_factor > max_load_factor_) {
507      Resize(Size() / ((min_load_factor_ + max_load_factor_) * 0.5));
508    }
509  }
510
511  // The hash set expands when Size() reaches ElementsUntilExpand().
512  size_t ElementsUntilExpand() const {
513    return elements_until_expand_;
514  }
515
516  size_t NumBuckets() const {
517    return num_buckets_;
518  }
519
520 private:
521  T& ElementForIndex(size_t index) {
522    DCHECK_LT(index, NumBuckets());
523    DCHECK(data_ != nullptr);
524    return data_[index];
525  }
526
527  const T& ElementForIndex(size_t index) const {
528    DCHECK_LT(index, NumBuckets());
529    DCHECK(data_ != nullptr);
530    return data_[index];
531  }
532
533  size_t IndexForHash(size_t hash) const {
534    // Protect against undefined behavior (division by zero).
535    if (UNLIKELY(num_buckets_ == 0)) {
536      return 0;
537    }
538    return hash % num_buckets_;
539  }
540
541  size_t NextIndex(size_t index) const {
542    if (UNLIKELY(++index >= num_buckets_)) {
543      DCHECK_EQ(index, NumBuckets());
544      return 0;
545    }
546    return index;
547  }
548
549  // Find the hash table slot for an element, or return NumBuckets() if not found.
550  // This value for not found is important so that iterator(this, FindIndex(...)) == end().
551  template <typename K>
552  size_t FindIndex(const K& element, size_t hash) const {
553    // Guard against failing to get an element for a non-existing index.
554    if (UNLIKELY(NumBuckets() == 0)) {
555      return 0;
556    }
557    DCHECK_EQ(hashfn_(element), hash);
558    size_t index = IndexForHash(hash);
559    while (true) {
560      const T& slot = ElementForIndex(index);
561      if (emptyfn_.IsEmpty(slot)) {
562        return NumBuckets();
563      }
564      if (pred_(slot, element)) {
565        return index;
566      }
567      index = NextIndex(index);
568    }
569  }
570
571  bool IsFreeSlot(size_t index) const {
572    return emptyfn_.IsEmpty(ElementForIndex(index));
573  }
574
575  // Allocate a number of buckets.
576  void AllocateStorage(size_t num_buckets) {
577    num_buckets_ = num_buckets;
578    data_ = allocfn_.allocate(num_buckets_);
579    owns_data_ = true;
580    for (size_t i = 0; i < num_buckets_; ++i) {
581      allocfn_.construct(allocfn_.address(data_[i]));
582      emptyfn_.MakeEmpty(data_[i]);
583    }
584  }
585
586  void DeallocateStorage() {
587    if (owns_data_) {
588      for (size_t i = 0; i < NumBuckets(); ++i) {
589        allocfn_.destroy(allocfn_.address(data_[i]));
590      }
591      if (data_ != nullptr) {
592        allocfn_.deallocate(data_, NumBuckets());
593      }
594      owns_data_ = false;
595    }
596    data_ = nullptr;
597    num_buckets_ = 0;
598  }
599
600  // Expand the set based on the load factors.
601  void Expand() {
602    size_t min_index = static_cast<size_t>(Size() / min_load_factor_);
603    // Resize based on the minimum load factor.
604    Resize(min_index);
605  }
606
607  // Expand / shrink the table to the new specified size.
608  void Resize(size_t new_size) {
609    if (new_size < kMinBuckets) {
610      new_size = kMinBuckets;
611    }
612    DCHECK_GE(new_size, Size());
613    T* const old_data = data_;
614    size_t old_num_buckets = num_buckets_;
615    // Reinsert all of the old elements.
616    const bool owned_data = owns_data_;
617    AllocateStorage(new_size);
618    for (size_t i = 0; i < old_num_buckets; ++i) {
619      T& element = old_data[i];
620      if (!emptyfn_.IsEmpty(element)) {
621        data_[FirstAvailableSlot(IndexForHash(hashfn_(element)))] = std::move(element);
622      }
623      if (owned_data) {
624        allocfn_.destroy(allocfn_.address(element));
625      }
626    }
627    if (owned_data) {
628      allocfn_.deallocate(old_data, old_num_buckets);
629    }
630
631    // When we hit elements_until_expand_, we are at the max load factor and must expand again.
632    elements_until_expand_ = NumBuckets() * max_load_factor_;
633  }
634
635  ALWAYS_INLINE size_t FirstAvailableSlot(size_t index) const {
636    DCHECK_LT(index, NumBuckets());  // Don't try to get a slot out of range.
637    size_t non_empty_count = 0;
638    while (!emptyfn_.IsEmpty(data_[index])) {
639      index = NextIndex(index);
640      non_empty_count++;
641      DCHECK_LE(non_empty_count, NumBuckets());  // Don't loop forever.
642    }
643    return index;
644  }
645
646  // Return new offset.
647  template <typename Elem>
648  static size_t WriteToBytes(uint8_t* ptr, size_t offset, Elem n) {
649    DCHECK_ALIGNED(ptr + offset, sizeof(n));
650    if (ptr != nullptr) {
651      *reinterpret_cast<Elem*>(ptr + offset) = n;
652    }
653    return offset + sizeof(n);
654  }
655
656  template <typename Elem>
657  static size_t ReadFromBytes(const uint8_t* ptr, size_t offset, Elem* out) {
658    DCHECK(ptr != nullptr);
659    DCHECK_ALIGNED(ptr + offset, sizeof(*out));
660    *out = *reinterpret_cast<const Elem*>(ptr + offset);
661    return offset + sizeof(*out);
662  }
663
664  Alloc allocfn_;  // Allocator function.
665  HashFn hashfn_;  // Hashing function.
666  EmptyFn emptyfn_;  // IsEmpty/SetEmpty function.
667  Pred pred_;  // Equals function.
668  size_t num_elements_;  // Number of inserted elements.
669  size_t num_buckets_;  // Number of hash table buckets.
670  size_t elements_until_expand_;  // Maximum number of elements until we expand the table.
671  bool owns_data_;  // If we own data_ and are responsible for freeing it.
672  T* data_;  // Backing storage.
673  double min_load_factor_;
674  double max_load_factor_;
675};
676
677template <class T, class EmptyFn, class HashFn, class Pred, class Alloc>
678void swap(HashSet<T, EmptyFn, HashFn, Pred, Alloc>& lhs,
679          HashSet<T, EmptyFn, HashFn, Pred, Alloc>& rhs) {
680  lhs.swap(rhs);
681}
682
683}  // namespace art
684
685#endif  // ART_RUNTIME_BASE_HASH_SET_H_
686