diff --git a/src/gpgmm/utils/StableList.h b/src/gpgmm/utils/StableList.h new file mode 100644 index 000000000..e60265b9e --- /dev/null +++ b/src/gpgmm/utils/StableList.h @@ -0,0 +1,341 @@ +// Copyright 2021 The GPGMM Authors +// +// 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. + +#ifndef GPGMM_UTILS_STABLELIST_H_ +#define GPGMM_UTILS_STABLELIST_H_ + +#include "Assert.h" +#include "Compiler.h" + +#include +#include +#include + +namespace gpgmm { + + /** \brief StableList is like a STL vector, offering reference stability. StableList is used + to contain data like a linked-list but is stored using one or more STL vectors (See + the Q&A section to understand how this differs from std::vector or std::list). + + Insert or remove from the front is O(1), from elsewhere O(N), where random deletion is allowed + but slow. If you need fast random insertion, use another data structure. + + To insert elements to the list, use push_back or emplace_back: + \code + StableList myList; + + MyItemT myItem = ...; + myList.push_back(item1); // Or + myList.emplace_back(...); + \endcode + + To remove any element from the list: + \code + myList.erase(index); or // erase(iterator) + \endcode + + To iterate through the list forwards: + \code + for (auto& it : list) { + ... + } + \endcode + + Questions and Answers: + + Q. When should I use StableList over a linked-list? + + A. The main resource to use StableList over a linked-list is to improve performance by using + contiguous memory for storage. A StableList will almost be always faster to iterate, insert, and + remove elements **unless** the majority elements reside in the middle of the list. + + Q. How does StableList implementation differ from std::vector? + + A. StableList is similar to STL vector except it is stable. Stable means pointers or + references to elements are guarenteed to remain valid where removing any element does not + invalidate others. And unlike a std::vector, the cost of growth is predicable and constant. + + Under the hood, StableList maintains two contigious allocated containers: a "dirty bit" array + for random deletion, and a normal std::vector for the data. + */ + template + class StableList { + public: + struct Chunk { + std::vector mData; + std::array mUsed = {}; // For random deletion. + }; + + StableList() = default; + + // Contructs the container with count default-inserted instances of T. + explicit StableList(size_t count) { + for (size_t i = 0; i < count; ++i) { + emplace_back(); + } + } + + // Contructs the container with count copies of value |value|. + explicit StableList(size_t count, const T& value) { + for (size_t i = 0; i < count; ++i) { + push_back(value); + } + } + + void shrink_to_fit() { + for (size_t i = mChunks.size(); i > 0; i--) { + auto& chunk = mChunks[i - 1]; + for (size_t j = chunk->mData.size(); j > 0; j--) { + if (!chunk->mUsed[j - 1]) { + chunk->mData.pop_back(); + chunk->mUsed[j - 1] = false; + } else { + return; + } + } + if (chunk->mData.size() == 0) { + mChunks.pop_back(); + } + } + } + + void pop_back() { + erase(GetSizeWithUnused() - 1); + } + + // Erasing from a position other then the end will not erase immediately. Not until + // no other positions after it are also erased. + void erase(size_t index) { + ASSERT(!empty()); + bool* isUsed = &mChunks[index / ChunkSize]->mUsed[index % ChunkSize]; + if (!*isUsed) { + return; // Already erased. + } + + *isUsed = false; + + // Erasing anywhere other then the end will cause the underlying vector to reallocate + // and invalidate iterators/references AFTER the point of erase. To prevent this, we + // shrink the size from the end, using the free bit vector to always keep the last one + // valid (if not empty). + shrink_to_fit(); + + mSize--; + } + + void push_back(const T& lvalue) { + Chunk& chunk = GetOrAddLastChunk(); + chunk.mData.push_back(lvalue); + chunk.mUsed[chunk.mData.size() - 1] = true; + mSize++; + } + + void push_back(T&& rvalue) { + Chunk& chunk = GetOrAddLastChunk(); + chunk.mData.push_back(std::move(rvalue)); + chunk.mUsed[chunk.mData.size() - 1] = true; + mSize++; + } + + template + void emplace_back(Args&&... args) { + Chunk& chunk = GetOrAddLastChunk(); + chunk.mData.emplace_back(std::forward(args)...); + chunk.mUsed[chunk.mData.size() - 1] = true; + mSize++; + } + + T& back() { + return mChunks.back()->mData.back(); + } + + const T& back() const { + return mChunks.back()->mData.back(); + } + + size_t capacity() const { + return mChunks.size() * ChunkSize; + } + + size_t size() const { + return mSize; + } + + size_t max_size() const { + return std::numeric_limits::max(); + } + + bool empty() const { + return size() == 0; + } + + T& operator[](size_t index) { + return mChunks[index / ChunkSize]->mData[index % ChunkSize]; + } + + const T& operator[](size_t index) const { + return const_cast&>(*this)[index]; + } + + bool operator!=(const StableList& other) const { + return !operator==(other); + } + + bool operator==(const StableList& other) const { + return size() == other.size() && std::equal(cbegin(), cend(), other.cbegin()); + } + + void reserve(size_t newCapacity) { + while (capacity() < newCapacity) { + AddChunk(); + } + } + + private: + // Holes in the middle of each chunk are not reported by size() but must otherwise be + // indexed for operations to work (eg. erase, enumeration). + size_t GetSizeWithUnused() const { + ASSERT(!empty()); + return (mChunks.size() - 1) * ChunkSize + mChunks.back()->mData.size(); + } + + Chunk& GetOrAddLastChunk() { + if (GPGMM_UNLIKELY(mChunks.empty() || + mChunks.back()->mData.size() == ChunkSize)) { // empty or full + AddChunk(); + } + return *mChunks.back(); + } + + void AddChunk() { + auto chunk = std::make_unique(); + // Stability is only guarenteed if there is reallocation and since reallocation is only + // possible when size == capacity, simply reserve it upfront. + chunk->mData.reserve(ChunkSize); + mChunks.push_back(std::move(chunk)); + } + + using storage_type = std::vector>; + storage_type mChunks; + + size_t mSize = 0; + + template + struct StableListIteratorBase { + StableListIteratorBase(StableListT* list = nullptr, size_t index = 0) + : mList(list), mIndex(index) { + } + + T& operator*() const { + return (*this->mList)[this->mIndex]; + } + + StableListIteratorBase& operator++() { + mIndex++; + for (size_t i = mIndex / ChunkSize; i < mList->mChunks.size(); i++) { + auto& chunk = mList->mChunks[i]; + for (size_t j = mIndex % ChunkSize; j < chunk->mData.size(); j++) { + if (chunk->mUsed[j]) { + return *this; + } + mIndex++; + } + } + return *this; + } + + bool operator==(const StableListIteratorBase& it) const { + return mList == it.mList && mIndex == it.mIndex; + } + + protected: + StableListT* mList; + size_t mIndex; + }; + + public: + struct iterator : public StableListIteratorBase> { + iterator(StableList* list, size_t index = 0) + : StableListIteratorBase>(list, index) { + } + + T& operator*() { + return (*this->mList)[this->mIndex]; + } + + bool operator==(const iterator& it) const { + return StableListIteratorBase>::operator==(it); + } + + bool operator!=(const iterator& it) const { + return !operator==(it); + } + }; + + struct const_iterator : public StableListIteratorBase> { + const_iterator(const iterator& it) + : StableListIteratorBase>(it.mList, it.mIndex) { + } + + const T& operator*() const { + return (*this->mList)[this->mIndex]; + } + + bool operator==(const const_iterator& it) const { + return StableListIteratorBase>::operator==(it); + } + + bool operator!=(const const_iterator& it) const { + return !operator==(it); + } + }; + + void erase(const_iterator it) { + erase(it.mIndex); + } + + iterator begin() { + return {this, 0}; + } + + const_iterator begin() const { + return {this, 0}; + } + + const_iterator cbegin() const { + return begin(); + } + + iterator end() { + if (GPGMM_UNLIKELY(empty())) { + return {this, 0}; + } + return {this, GetSizeWithUnused() - 1}; + } + + const_iterator end() const { + if (GPGMM_UNLIKELY(empty())) { + return {this, 0}; + } + return {this, GetSizeWithUnused() - 1}; + } + + const_iterator cend() const { + return end(); + } + }; + +} // namespace gpgmm + +#endif // GPGMM_UTILS_STABLELIST_H_ diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn index b8055c35c..9222b895a 100644 --- a/src/tests/BUILD.gn +++ b/src/tests/BUILD.gn @@ -119,6 +119,7 @@ test("gpgmm_unittests") { "unittests/SegmentedMemoryAllocatorTests.cpp", "unittests/SlabBlockAllocatorTests.cpp", "unittests/SlabMemoryAllocatorTests.cpp", + "unittests/StableListTests.cpp", "unittests/UtilsTest.cpp", ] diff --git a/src/tests/unittests/StableListTests.cpp b/src/tests/unittests/StableListTests.cpp new file mode 100644 index 000000000..f28e029cf --- /dev/null +++ b/src/tests/unittests/StableListTests.cpp @@ -0,0 +1,239 @@ +// Copyright 2021 The GPGMM Authors +// +// 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 + +#include "gpgmm/utils/StableList.h" + +using namespace gpgmm; + +TEST(StableListTests, Create) { + { + StableList list(4096); + EXPECT_EQ(list.size(), 4096u); + } + + { + StableList list(4096, 0xdeadbeef); + EXPECT_EQ(list.size(), 4096u); + } +} + +TEST(StableListTests, Reserve) { + StableList list; + + // Reserve exactly the chunk size. + list.reserve(1024); + EXPECT_EQ(list.capacity(), 1024u); + + // One over rounds up to nearest chunk. + list.reserve(1025); + EXPECT_EQ(list.capacity(), 2048u); +} + +TEST(StableListTests, Append) { + StableList list; + list.push_back(0); + list.push_back(1); + + EXPECT_EQ(list.capacity(), 2u); + EXPECT_EQ(list.size(), 2u); + + list.push_back(3); + list.push_back(4); + + EXPECT_EQ(list.capacity(), 4u); + EXPECT_EQ(list.size(), 4u); +} + +TEST(StableListTests, Insert) { + StableList list; + list.push_back(0); + list.push_back(1); + list.push_back(2); + + list[0] = 2; + list[1] = 1; + list[2] = 0; + + EXPECT_EQ(list[0], 2); + EXPECT_EQ(list[1], 1); + EXPECT_EQ(list[2], 0); +} + +TEST(StableListTests, RemoveEnds) { + StableList list; + list.push_back(0); + list.push_back(1); + list.push_back(2); + + EXPECT_EQ(list.size(), 3u); + + list.pop_back(); + list.pop_back(); + list.pop_back(); + + EXPECT_EQ(list.size(), 0u); +} + +TEST(StableListTests, RemoveSame) { + StableList list; + list.push_back(0); + list.push_back(1); + + list.erase(0); + list.erase(0); // No-op! + + EXPECT_EQ(list.size(), 1u); +} + +TEST(StableListTests, RemoveMiddle) { + // Before = [] + // After = [0,1,2,3,4] + StableList list; + list.push_back(0); + list.push_back(1); + list.push_back(2); + list.push_back(3); + list.push_back(4); + + // Before = [0,1,2,3,4] + // After = [0,_,2,_,4] + list.erase(1); // Middle of chunk#1 + list.erase(3); // Middle of chunk#2 + + // Before = [0,_,2,_,4] + // After = [0,_,2] + list.pop_back(); + EXPECT_EQ(list.back(), 2); + + // Before = [0,_,2] + // After = [0,] + list.pop_back(); + EXPECT_EQ(list.back(), 0); +} + +TEST(StableListTests, Growth) { + std::vector ptrs = {}; + StableList list; + for (size_t i = 0; i < 2048; i++) { + list.push_back(i); + ptrs.push_back(&list[i]); + } + + for (size_t i = 0; i < list.size(); i++) { + EXPECT_EQ(*ptrs[i], list[i]); + } + + // Discard last half. + for (size_t i = 0; i < list.size() / 2; i++) { + list.pop_back(); + ptrs.pop_back(); + } + + for (size_t i = 0; i < list.size(); i++) { + EXPECT_EQ(*ptrs[i], list[i]); + } +} + +TEST(StableListTests, Enumerate) { + // Over a list with a single chunk. + { + StableList list; + list.push_back(0); + list.push_back(1); + list.push_back(2); + list.push_back(3); + + int i = 0; + for (auto& it : list) { + EXPECT_EQ(it, i++); + } + } + + // Over a list with multiple chunks. + { + StableList list; + list.push_back(0); + list.push_back(1); + list.push_back(2); + list.push_back(3); + list.push_back(4); + + int i = 0; + for (auto& it : list) { + EXPECT_EQ(it, i++); + } + } + + // Over a list with holes in odd indexes. + { + StableList list; + list.push_back(0); + list.push_back(0xdeadbeef); + list.push_back(2); + list.push_back(0xdeadbeef); + list.push_back(4); + + list.erase(1); + list.erase(3); + + int i = 0; + for (auto& it : list) { + EXPECT_EQ(it, i); + i += 2; + } + } + + // Over a list with holes of various sizes. + { + StableList list; + list.push_back(0); + list.push_back(0xdeadbeef); + list.push_back(2); + list.push_back(3); + list.push_back(0xdeadbeef); + list.push_back(0xdeadbeef); + list.push_back(6); + list.push_back(7); + list.push_back(8); + + list.erase(1); + list.erase(4); + list.erase(5); + + int i = 0; + for (auto& it : list) { + if (i != 1 && i != 4 && i != 5) { + EXPECT_EQ(it, i++); + } + } + } + + // Over a list made empty. + { + StableList list; + list.push_back(0xdeadbeef); + list.push_back(0xdeadbeef); + + list.erase(0); + list.erase(1); + + int i = 0; + for (auto& _ : list) { + ASSERT_TRUE(_); + } + EXPECT_EQ(i, 0); + } +}