| // Copyright (c) 2023 Google Inc. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include <stddef.h> | |
| #include <algorithm> | |
| #include <cassert> | |
| #include <cstdint> | |
| #include <functional> | |
| #include <initializer_list> | |
| #include <iterator> | |
| #include <limits> | |
| #include <type_traits> | |
| #include <vector> | |
| #ifndef SOURCE_ENUM_SET_H_ | |
| #define SOURCE_ENUM_SET_H_ | |
| #include "source/latest_version_spirv_header.h" | |
| namespace spvtools { | |
| // This container is optimized to store and retrieve unsigned enum values. | |
| // The base model for this implementation is an open-addressing hashtable with | |
| // linear probing. For small enums (max index < 64), all operations are O(1). | |
| // | |
| // - Enums are stored in buckets (64 contiguous values max per bucket) | |
| // - Buckets ranges don't overlap, but don't have to be contiguous. | |
| // - Enums are packed into 64-bits buckets, using 1 bit per enum value. | |
| // | |
| // Example: | |
| // - MyEnum { A = 0, B = 1, C = 64, D = 65 } | |
| // - 2 buckets are required: | |
| // - bucket 0, storing values in the range [ 0; 64[ | |
| // - bucket 1, storing values in the range [64; 128[ | |
| // | |
| // - Buckets are stored in a sorted vector (sorted by bucket range). | |
| // - Retrieval is done by computing the theoretical bucket index using the enum | |
| // value, and | |
| // doing a linear scan from this position. | |
| // - Insertion is done by retrieving the bucket and either: | |
| // - inserting a new bucket in the sorted vector when no buckets has a | |
| // compatible range. | |
| // - setting the corresponding bit in the bucket. | |
| // This means insertion in the middle/beginning can cause a memmove when no | |
| // bucket is available. In our case, this happens at most 23 times for the | |
| // largest enum we have (Opcodes). | |
| template <typename T> | |
| class EnumSet { | |
| private: | |
| using BucketType = uint64_t; | |
| using ElementType = std::underlying_type_t<T>; | |
| static_assert(std::is_enum_v<T>, "EnumSets only works with enums."); | |
| static_assert(std::is_signed_v<ElementType> == false, | |
| "EnumSet doesn't supports signed enums."); | |
| // Each bucket can hold up to `kBucketSize` distinct, contiguous enum values. | |
| // The first value a bucket can hold must be aligned on `kBucketSize`. | |
| struct Bucket { | |
| // bit mask to store `kBucketSize` enums. | |
| BucketType data; | |
| // 1st enum this bucket can represent. | |
| T start; | |
| friend bool operator==(const Bucket& lhs, const Bucket& rhs) { | |
| return lhs.start == rhs.start && lhs.data == rhs.data; | |
| } | |
| }; | |
| // How many distinct values can a bucket hold? 1 bit per value. | |
| static constexpr size_t kBucketSize = sizeof(BucketType) * 8ULL; | |
| public: | |
| class Iterator { | |
| public: | |
| typedef Iterator self_type; | |
| typedef T value_type; | |
| typedef T& reference; | |
| typedef T* pointer; | |
| typedef std::forward_iterator_tag iterator_category; | |
| typedef size_t difference_type; | |
| Iterator(const Iterator& other) | |
| : set_(other.set_), | |
| bucketIndex_(other.bucketIndex_), | |
| bucketOffset_(other.bucketOffset_) {} | |
| Iterator& operator++() { | |
| do { | |
| if (bucketIndex_ >= set_->buckets_.size()) { | |
| bucketIndex_ = set_->buckets_.size(); | |
| bucketOffset_ = 0; | |
| break; | |
| } | |
| if (bucketOffset_ + 1 == kBucketSize) { | |
| bucketOffset_ = 0; | |
| ++bucketIndex_; | |
| } else { | |
| ++bucketOffset_; | |
| } | |
| } while (bucketIndex_ < set_->buckets_.size() && | |
| !set_->HasEnumAt(bucketIndex_, bucketOffset_)); | |
| return *this; | |
| } | |
| Iterator operator++(int) { | |
| Iterator old = *this; | |
| operator++(); | |
| return old; | |
| } | |
| T operator*() const { | |
| assert(set_->HasEnumAt(bucketIndex_, bucketOffset_) && | |
| "operator*() called on an invalid iterator."); | |
| return GetValueFromBucket(set_->buckets_[bucketIndex_], bucketOffset_); | |
| } | |
| bool operator!=(const Iterator& other) const { | |
| return set_ != other.set_ || bucketOffset_ != other.bucketOffset_ || | |
| bucketIndex_ != other.bucketIndex_; | |
| } | |
| bool operator==(const Iterator& other) const { | |
| return !(operator!=(other)); | |
| } | |
| Iterator& operator=(const Iterator& other) { | |
| set_ = other.set_; | |
| bucketIndex_ = other.bucketIndex_; | |
| bucketOffset_ = other.bucketOffset_; | |
| return *this; | |
| } | |
| private: | |
| Iterator(const EnumSet* set, size_t bucketIndex, ElementType bucketOffset) | |
| : set_(set), bucketIndex_(bucketIndex), bucketOffset_(bucketOffset) {} | |
| private: | |
| const EnumSet* set_ = nullptr; | |
| // Index of the bucket in the vector. | |
| size_t bucketIndex_ = 0; | |
| // Offset in bits in the current bucket. | |
| ElementType bucketOffset_ = 0; | |
| friend class EnumSet; | |
| }; | |
| // Required to allow the use of std::inserter. | |
| using value_type = T; | |
| using const_iterator = Iterator; | |
| using iterator = Iterator; | |
| public: | |
| iterator cbegin() const noexcept { | |
| auto it = iterator(this, /* bucketIndex= */ 0, /* bucketOffset= */ 0); | |
| if (buckets_.size() == 0) { | |
| return it; | |
| } | |
| // The iterator has the logic to find the next valid bit. If the value 0 | |
| // is not stored, use it to find the next valid bit. | |
| if (!HasEnumAt(it.bucketIndex_, it.bucketOffset_)) { | |
| ++it; | |
| } | |
| return it; | |
| } | |
| iterator begin() const noexcept { return cbegin(); } | |
| iterator cend() const noexcept { | |
| return iterator(this, buckets_.size(), /* bucketOffset= */ 0); | |
| } | |
| iterator end() const noexcept { return cend(); } | |
| // Creates an empty set. | |
| EnumSet() : buckets_(0), size_(0) {} | |
| // Creates a set and store `value` in it. | |
| EnumSet(T value) : EnumSet() { insert(value); } | |
| // Creates a set and stores each `values` in it. | |
| EnumSet(std::initializer_list<T> values) : EnumSet() { | |
| for (auto item : values) { | |
| insert(item); | |
| } | |
| } | |
| // Creates a set, and insert `count` enum values pointed by `array` in it. | |
| EnumSet(ElementType count, const T* array) : EnumSet() { | |
| for (ElementType i = 0; i < count; i++) { | |
| insert(array[i]); | |
| } | |
| } | |
| // Creates a set initialized with the content of the range [begin; end[. | |
| template <class InputIt> | |
| EnumSet(InputIt begin, InputIt end) : EnumSet() { | |
| for (; begin != end; ++begin) { | |
| insert(*begin); | |
| } | |
| } | |
| // Copies the EnumSet `other` into a new EnumSet. | |
| EnumSet(const EnumSet& other) | |
| : buckets_(other.buckets_), size_(other.size_) {} | |
| // Moves the EnumSet `other` into a new EnumSet. | |
| EnumSet(EnumSet&& other) | |
| : buckets_(std::move(other.buckets_)), size_(other.size_) {} | |
| // Deep-copies the EnumSet `other` into this EnumSet. | |
| EnumSet& operator=(const EnumSet& other) { | |
| buckets_ = other.buckets_; | |
| size_ = other.size_; | |
| return *this; | |
| } | |
| // Matches std::unordered_set::insert behavior. | |
| std::pair<iterator, bool> insert(const T& value) { | |
| const size_t index = FindBucketForValue(value); | |
| const ElementType offset = ComputeBucketOffset(value); | |
| if (index >= buckets_.size() || | |
| buckets_[index].start != ComputeBucketStart(value)) { | |
| size_ += 1; | |
| InsertBucketFor(index, value); | |
| return std::make_pair(Iterator(this, index, offset), true); | |
| } | |
| auto& bucket = buckets_[index]; | |
| const auto mask = ComputeMaskForValue(value); | |
| if (bucket.data & mask) { | |
| return std::make_pair(Iterator(this, index, offset), false); | |
| } | |
| size_ += 1; | |
| bucket.data |= ComputeMaskForValue(value); | |
| return std::make_pair(Iterator(this, index, offset), true); | |
| } | |
| // Inserts `value` in the set if possible. | |
| // Similar to `std::unordered_set::insert`, except the hint is ignored. | |
| // Returns an iterator to the inserted element, or the element preventing | |
| // insertion. | |
| iterator insert(const_iterator, const T& value) { | |
| return insert(value).first; | |
| } | |
| // Inserts `value` in the set if possible. | |
| // Similar to `std::unordered_set::insert`, except the hint is ignored. | |
| // Returns an iterator to the inserted element, or the element preventing | |
| // insertion. | |
| iterator insert(const_iterator, T&& value) { return insert(value).first; } | |
| // Inserts all the values in the range [`first`; `last[. | |
| // Similar to `std::unordered_set::insert`. | |
| template <class InputIt> | |
| void insert(InputIt first, InputIt last) { | |
| for (auto it = first; it != last; ++it) { | |
| insert(*it); | |
| } | |
| } | |
| // Removes the value `value` into the set. | |
| // Similar to `std::unordered_set::erase`. | |
| // Returns the number of erased elements. | |
| size_t erase(const T& value) { | |
| const size_t index = FindBucketForValue(value); | |
| if (index >= buckets_.size() || | |
| buckets_[index].start != ComputeBucketStart(value)) { | |
| return 0; | |
| } | |
| auto& bucket = buckets_[index]; | |
| const auto mask = ComputeMaskForValue(value); | |
| if (!(bucket.data & mask)) { | |
| return 0; | |
| } | |
| size_ -= 1; | |
| bucket.data &= ~mask; | |
| if (bucket.data == 0) { | |
| buckets_.erase(buckets_.cbegin() + index); | |
| } | |
| return 1; | |
| } | |
| // Returns true if `value` is present in the set. | |
| bool contains(T value) const { | |
| const size_t index = FindBucketForValue(value); | |
| if (index >= buckets_.size() || | |
| buckets_[index].start != ComputeBucketStart(value)) { | |
| return false; | |
| } | |
| auto& bucket = buckets_[index]; | |
| return bucket.data & ComputeMaskForValue(value); | |
| } | |
| // Returns the 1 if `value` is present in the set, `0` otherwise. | |
| inline size_t count(T value) const { return contains(value) ? 1 : 0; } | |
| // Returns true if the set is holds no values. | |
| inline bool empty() const { return size_ == 0; } | |
| // Returns the number of enums stored in this set. | |
| size_t size() const { return size_; } | |
| // Returns true if this set contains at least one value contained in `in_set`. | |
| // Note: If `in_set` is empty, this function returns true. | |
| bool HasAnyOf(const EnumSet<T>& in_set) const { | |
| if (in_set.empty()) { | |
| return true; | |
| } | |
| auto lhs = buckets_.cbegin(); | |
| auto rhs = in_set.buckets_.cbegin(); | |
| while (lhs != buckets_.cend() && rhs != in_set.buckets_.cend()) { | |
| if (lhs->start == rhs->start) { | |
| if (lhs->data & rhs->data) { | |
| // At least 1 bit is shared. Early return. | |
| return true; | |
| } | |
| lhs++; | |
| rhs++; | |
| continue; | |
| } | |
| // LHS bucket is smaller than the current RHS bucket. Catching up on RHS. | |
| if (lhs->start < rhs->start) { | |
| lhs++; | |
| continue; | |
| } | |
| // Otherwise, RHS needs to catch up on LHS. | |
| rhs++; | |
| } | |
| return false; | |
| } | |
| private: | |
| // Returns the index of the last bucket in which `value` could be stored. | |
| static constexpr inline size_t ComputeLargestPossibleBucketIndexFor(T value) { | |
| return static_cast<size_t>(value) / kBucketSize; | |
| } | |
| // Returns the smallest enum value that could be contained in the same bucket | |
| // as `value`. | |
| static constexpr inline T ComputeBucketStart(T value) { | |
| return static_cast<T>(kBucketSize * | |
| ComputeLargestPossibleBucketIndexFor(value)); | |
| } | |
| // Returns the index of the bit that corresponds to `value` in the bucket. | |
| static constexpr inline ElementType ComputeBucketOffset(T value) { | |
| return static_cast<ElementType>(value) % kBucketSize; | |
| } | |
| // Returns the bitmask used to represent the enum `value` in its bucket. | |
| static constexpr inline BucketType ComputeMaskForValue(T value) { | |
| return 1ULL << ComputeBucketOffset(value); | |
| } | |
| // Returns the `enum` stored in `bucket` at `offset`. | |
| // `offset` is the bit-offset in the bucket storage. | |
| static constexpr inline T GetValueFromBucket(const Bucket& bucket, | |
| BucketType offset) { | |
| return static_cast<T>(static_cast<ElementType>(bucket.start) + offset); | |
| } | |
| // For a given enum `value`, finds the bucket index that could contain this | |
| // value. If no such bucket is found, the index at which the new bucket should | |
| // be inserted is returned. | |
| size_t FindBucketForValue(T value) const { | |
| // Set is empty, insert at 0. | |
| if (buckets_.size() == 0) { | |
| return 0; | |
| } | |
| const T wanted_start = ComputeBucketStart(value); | |
| assert(buckets_.size() > 0 && | |
| "Size must not be 0 here. Has the code above changed?"); | |
| size_t index = std::min(buckets_.size() - 1, | |
| ComputeLargestPossibleBucketIndexFor(value)); | |
| // This loops behaves like std::upper_bound with a reverse iterator. | |
| // Buckets are sorted. 3 main cases: | |
| // - The bucket matches | |
| // => returns the bucket index. | |
| // - The found bucket is larger | |
| // => scans left until it finds the correct bucket, or insertion point. | |
| // - The found bucket is smaller | |
| // => We are at the end, so we return past-end index for insertion. | |
| for (; buckets_[index].start >= wanted_start; index--) { | |
| if (index == 0) { | |
| return 0; | |
| } | |
| } | |
| return index + 1; | |
| } | |
| // Creates a new bucket to store `value` and inserts it at `index`. | |
| // If the `index` is past the end, the bucket is inserted at the end of the | |
| // vector. | |
| void InsertBucketFor(size_t index, T value) { | |
| const T bucket_start = ComputeBucketStart(value); | |
| Bucket bucket = {1ULL << ComputeBucketOffset(value), bucket_start}; | |
| auto it = buckets_.emplace(buckets_.begin() + index, std::move(bucket)); | |
| #if defined(NDEBUG) | |
| (void)it; // Silencing unused variable warning. | |
| #else | |
| assert(std::next(it) == buckets_.end() || | |
| std::next(it)->start > bucket_start); | |
| assert(it == buckets_.begin() || std::prev(it)->start < bucket_start); | |
| #endif | |
| } | |
| // Returns true if the bucket at `bucketIndex/ stores the enum at | |
| // `bucketOffset`, false otherwise. | |
| bool HasEnumAt(size_t bucketIndex, BucketType bucketOffset) const { | |
| assert(bucketIndex < buckets_.size()); | |
| assert(bucketOffset < kBucketSize); | |
| return buckets_[bucketIndex].data & (1ULL << bucketOffset); | |
| } | |
| // Returns true if `lhs` and `rhs` hold the exact same values. | |
| friend bool operator==(const EnumSet& lhs, const EnumSet& rhs) { | |
| if (lhs.size_ != rhs.size_) { | |
| return false; | |
| } | |
| if (lhs.buckets_.size() != rhs.buckets_.size()) { | |
| return false; | |
| } | |
| return lhs.buckets_ == rhs.buckets_; | |
| } | |
| // Returns true if `lhs` and `rhs` hold at least 1 different value. | |
| friend bool operator!=(const EnumSet& lhs, const EnumSet& rhs) { | |
| return !(lhs == rhs); | |
| } | |
| // Storage for the buckets. | |
| std::vector<Bucket> buckets_; | |
| // How many enums is this set storing. | |
| size_t size_ = 0; | |
| }; | |
| // A set of spv::Capability. | |
| using CapabilitySet = EnumSet<spv::Capability>; | |
| } // namespace spvtools | |
| #endif // SOURCE_ENUM_SET_H_ |