diff --git a/BUILD.gn b/BUILD.gn index c5981fac72..3de92a47ce 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -833,11 +833,13 @@ test("dawn_unittests") { "src/tests/unittests/MathTests.cpp", "src/tests/unittests/ObjectBaseTests.cpp", "src/tests/unittests/PerStageTests.cpp", + "src/tests/unittests/PlacementAllocatedTests.cpp", "src/tests/unittests/RefCountedTests.cpp", "src/tests/unittests/ResultTests.cpp", "src/tests/unittests/RingBufferAllocatorTests.cpp", "src/tests/unittests/SerialMapTests.cpp", "src/tests/unittests/SerialQueueTests.cpp", + "src/tests/unittests/SlabAllocatorTests.cpp", "src/tests/unittests/SystemUtilsTests.cpp", "src/tests/unittests/ToBackendTests.cpp", "src/tests/unittests/validation/BindGroupValidationTests.cpp", diff --git a/src/common/BUILD.gn b/src/common/BUILD.gn index 14bcf0a29d..c036f62ce6 100644 --- a/src/common/BUILD.gn +++ b/src/common/BUILD.gn @@ -112,6 +112,7 @@ if (is_win || is_linux || is_mac || is_fuchsia || is_android) { "Log.h", "Math.cpp", "Math.h", + "PlacementAllocated.h", "Platform.h", "Result.cpp", "Result.h", @@ -119,6 +120,8 @@ if (is_win || is_linux || is_mac || is_fuchsia || is_android) { "SerialMap.h", "SerialQueue.h", "SerialStorage.h", + "SlabAllocator.cpp", + "SlabAllocator.h", "SwapChainUtils.h", "SystemUtils.cpp", "SystemUtils.h", diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 2498db638c..f36a9d02d0 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(dawn_common PRIVATE "Log.h" "Math.cpp" "Math.h" + "PlacementAllocated.h" "Platform.h" "Result.cpp" "Result.h" @@ -35,6 +36,8 @@ target_sources(dawn_common PRIVATE "SerialMap.h" "SerialQueue.h" "SerialStorage.h" + "SlabAllocator.cpp" + "SlabAllocator.h" "SwapChainUtils.h" "SystemUtils.cpp" "SystemUtils.h" diff --git a/src/common/PlacementAllocated.h b/src/common/PlacementAllocated.h new file mode 100644 index 0000000000..6bb329c3d5 --- /dev/null +++ b/src/common/PlacementAllocated.h @@ -0,0 +1,37 @@ +// Copyright 2020 The Dawn 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 COMMON_PLACEMENTALLOCATED_H_ +#define COMMON_PLACEMENTALLOCATED_H_ + +#include + +class PlacementAllocated { + public: + // Delete the default new operator so this can only be created with placement new. + void* operator new(size_t) = delete; + + void* operator new(size_t size, void* ptr) { + // Pass through the pointer of the allocation. This is essentially the default + // placement-new implementation, but we must define it if we delete the default + // new operator. + return ptr; + } + + void operator delete(void* ptr) { + // Object is placement-allocated. Don't free the memory. + } +}; + +#endif // COMMON_PLACEMENTALLOCATED_H_ diff --git a/src/common/SlabAllocator.cpp b/src/common/SlabAllocator.cpp new file mode 100644 index 0000000000..75aada5b68 --- /dev/null +++ b/src/common/SlabAllocator.cpp @@ -0,0 +1,234 @@ +// Copyright 2020 The Dawn 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 "common/SlabAllocator.h" + +#include "common/Assert.h" +#include "common/Math.h" + +#include +#include +#include + +// IndexLinkNode + +SlabAllocatorImpl::IndexLinkNode::IndexLinkNode(Index index, Index nextIndex) + : index(index), nextIndex(nextIndex) { +} + +// Slab + +SlabAllocatorImpl::Slab::Slab(std::unique_ptr allocation, IndexLinkNode* head) + : allocation(std::move(allocation)), + freeList(head), + prev(nullptr), + next(nullptr), + blocksInUse(0) { +} + +SlabAllocatorImpl::SentinelSlab::SentinelSlab() : Slab(nullptr, nullptr) { +} + +SlabAllocatorImpl::SentinelSlab::~SentinelSlab() { + Slab* slab = this->next; + while (slab != nullptr) { + Slab* next = slab->next; + ASSERT(slab->blocksInUse == 0); + slab->~Slab(); + slab = next; + } +} + +// SlabAllocatorImpl + +SlabAllocatorImpl::Index SlabAllocatorImpl::kInvalidIndex = + std::numeric_limits::max(); + +SlabAllocatorImpl::SlabAllocatorImpl(Index blocksPerSlab, + uint32_t allocationAlignment, + uint32_t slabBlocksOffset, + uint32_t blockStride, + uint32_t indexLinkNodeOffset) + : mAllocationAlignment(allocationAlignment), + mSlabBlocksOffset(slabBlocksOffset), + mBlockStride(blockStride), + mIndexLinkNodeOffset(indexLinkNodeOffset), + mBlocksPerSlab(blocksPerSlab), + mTotalAllocationSize( + // required allocation size + static_cast(mSlabBlocksOffset) + mBlocksPerSlab * mBlockStride + + // Pad the allocation size by mAllocationAlignment so that the aligned allocation still + // fulfills the required size. + mAllocationAlignment) { + ASSERT(IsPowerOfTwo(mAllocationAlignment)); +} + +SlabAllocatorImpl::~SlabAllocatorImpl() = default; + +SlabAllocatorImpl::IndexLinkNode* SlabAllocatorImpl::OffsetFrom( + IndexLinkNode* node, + std::make_signed_t offset) const { + return reinterpret_cast(reinterpret_cast(node) + + static_cast(mBlockStride) * offset); +} + +SlabAllocatorImpl::IndexLinkNode* SlabAllocatorImpl::NodeFromObject(void* object) const { + return reinterpret_cast(static_cast(object) + + mIndexLinkNodeOffset); +} + +void* SlabAllocatorImpl::ObjectFromNode(IndexLinkNode* node) const { + return static_cast(reinterpret_cast(node) - mIndexLinkNodeOffset); +} + +bool SlabAllocatorImpl::IsNodeInSlab(Slab* slab, IndexLinkNode* node) const { + char* firstObjectPtr = reinterpret_cast(slab) + mSlabBlocksOffset; + IndexLinkNode* firstNode = NodeFromObject(firstObjectPtr); + IndexLinkNode* lastNode = OffsetFrom(firstNode, mBlocksPerSlab - 1); + return node >= firstNode && node <= lastNode && node->index < mBlocksPerSlab; +} + +void SlabAllocatorImpl::PushFront(Slab* slab, IndexLinkNode* node) const { + ASSERT(IsNodeInSlab(slab, node)); + + IndexLinkNode* head = slab->freeList; + if (head == nullptr) { + node->nextIndex = kInvalidIndex; + } else { + ASSERT(IsNodeInSlab(slab, head)); + node->nextIndex = head->index; + } + slab->freeList = node; + + ASSERT(slab->blocksInUse != 0); + slab->blocksInUse--; +} + +SlabAllocatorImpl::IndexLinkNode* SlabAllocatorImpl::PopFront(Slab* slab) const { + ASSERT(slab->freeList != nullptr); + + IndexLinkNode* head = slab->freeList; + if (head->nextIndex == kInvalidIndex) { + slab->freeList = nullptr; + } else { + ASSERT(IsNodeInSlab(slab, head)); + slab->freeList = OffsetFrom(head, head->nextIndex - head->index); + ASSERT(IsNodeInSlab(slab, slab->freeList)); + } + + ASSERT(slab->blocksInUse < mBlocksPerSlab); + slab->blocksInUse++; + return head; +} + +void SlabAllocatorImpl::SentinelSlab::Prepend(SlabAllocatorImpl::Slab* slab) { + if (this->next != nullptr) { + this->next->prev = slab; + } + slab->prev = this; + slab->next = this->next; + this->next = slab; +} + +void SlabAllocatorImpl::Slab::Splice() { + SlabAllocatorImpl::Slab* originalPrev = this->prev; + SlabAllocatorImpl::Slab* originalNext = this->next; + + this->prev = nullptr; + this->next = nullptr; + + ASSERT(originalPrev != nullptr); + + // Set the originalNext's prev pointer. + if (originalNext != nullptr) { + originalNext->prev = originalPrev; + } + + // Now, set the originalNext as the originalPrev's new next. + originalPrev->next = originalNext; +} + +void* SlabAllocatorImpl::Allocate() { + if (mAvailableSlabs.next == nullptr) { + GetNewSlab(); + } + + Slab* slab = mAvailableSlabs.next; + IndexLinkNode* node = PopFront(slab); + ASSERT(node != nullptr); + + // Move full slabs to a separate list, so allocate can always return quickly. + if (slab->blocksInUse == mBlocksPerSlab) { + slab->Splice(); + mFullSlabs.Prepend(slab); + } + + return ObjectFromNode(node); +} + +void SlabAllocatorImpl::Deallocate(void* ptr) { + IndexLinkNode* node = NodeFromObject(ptr); + + ASSERT(node->index < mBlocksPerSlab); + void* firstAllocation = ObjectFromNode(OffsetFrom(node, -node->index)); + Slab* slab = reinterpret_cast(static_cast(firstAllocation) - mSlabBlocksOffset); + ASSERT(slab != nullptr); + + bool slabWasFull = slab->blocksInUse == mBlocksPerSlab; + + ASSERT(slab->blocksInUse != 0); + PushFront(slab, node); + + if (slabWasFull) { + // Slab is in the full list. Move it to the recycled list. + ASSERT(slab->freeList != nullptr); + slab->Splice(); + mRecycledSlabs.Prepend(slab); + } + + // TODO(enga): Occasionally prune slabs if |blocksInUse == 0|. + // Doing so eagerly hurts performance. +} + +void SlabAllocatorImpl::GetNewSlab() { + // Should only be called when there are no available slabs. + ASSERT(mAvailableSlabs.next == nullptr); + + if (mRecycledSlabs.next != nullptr) { + // If the recycled list is non-empty, swap their contents. + std::swap(mAvailableSlabs.next, mRecycledSlabs.next); + + // We swapped the next pointers, so the prev pointer is wrong. + // Update it here. + mAvailableSlabs.next->prev = &mAvailableSlabs; + ASSERT(mRecycledSlabs.next == nullptr); + return; + } + + // TODO(enga): Use aligned_alloc with C++17. + auto allocation = std::unique_ptr(new char[mTotalAllocationSize]); + char* alignedPtr = AlignPtr(allocation.get(), mAllocationAlignment); + + char* dataStart = alignedPtr + mSlabBlocksOffset; + + IndexLinkNode* node = NodeFromObject(dataStart); + for (uint32_t i = 0; i < mBlocksPerSlab; ++i) { + new (OffsetFrom(node, i)) IndexLinkNode(i, i + 1); + } + + IndexLinkNode* lastNode = OffsetFrom(node, mBlocksPerSlab - 1); + lastNode->nextIndex = kInvalidIndex; + + mAvailableSlabs.Prepend(new (alignedPtr) Slab(std::move(allocation), node)); +} diff --git a/src/common/SlabAllocator.h b/src/common/SlabAllocator.h new file mode 100644 index 0000000000..59c9b63f75 --- /dev/null +++ b/src/common/SlabAllocator.h @@ -0,0 +1,202 @@ +// Copyright 2020 The Dawn 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 COMMON_SLABALLOCATOR_H_ +#define COMMON_SLABALLOCATOR_H_ + +#include "common/PlacementAllocated.h" + +#include +#include +#include + +// The SlabAllocator allocates objects out of one or more fixed-size contiguous "slabs" of memory. +// This makes it very quick to allocate and deallocate fixed-size objects because the allocator only +// needs to index an offset into pre-allocated memory. It is similar to a pool-allocator that +// recycles memory from previous allocations, except multiple allocations are hosted contiguously in +// one large slab. +// +// Internally, the SlabAllocator stores slabs as a linked list to avoid extra indirections indexing +// into an std::vector. To service an allocation request, the allocator only needs to know the first +// currently available slab. There are three backing linked lists: AVAILABLE, FULL, and RECYCLED. +// A slab that is AVAILABLE can be used to immediately service allocation requests. Once it has no +// remaining space, it is moved to the FULL state. When a FULL slab sees any deallocations, it is +// moved to the RECYCLED state. The RECYCLED state is separate from the AVAILABLE state so that +// deallocations don't immediately prepend slabs to the AVAILABLE list, and change the current slab +// servicing allocations. When the AVAILABLE list becomes empty is it swapped with the RECYCLED +// list. +// +// Allocated objects are placement-allocated with some extra info at the end (we'll call the Object +// plus the extra bytes a "block") used to specify the constant index of the block in its parent +// slab, as well as the index of the next available block. So, following the block next-indices +// forms a linked list of free blocks. +// +// Slab creation: When a new slab is allocated, sufficient memory is allocated for it, and then the +// slab metadata plus all of its child blocks are placement-allocated into the memory. Indices and +// next-indices are initialized to form the free-list of blocks. +// +// Allocation: When an object is allocated, if there is no space available in an existing slab, a +// new slab is created (or an old slab is recycled). The first block of the slab is removed and +// returned. +// +// Deallocation: When an object is deallocated, it can compute the pointer to its parent slab +// because it stores the index of its own allocation. That block is then prepended to the slab's +// free list. +class SlabAllocatorImpl { + public: + // Allocations host their current index and the index of the next free block. + // Because this is an index, and not a byte offset, it can be much smaller than a size_t. + // TODO(enga): Is uint8_t sufficient? + using Index = uint16_t; + + protected: + // This is essentially a singly linked list using indices instead of pointers, + // so we store the index of "this" in |this->index|. + struct IndexLinkNode : PlacementAllocated { + IndexLinkNode(Index index, Index nextIndex); + + const Index index; // The index of this block in the slab. + Index nextIndex; // The index of the next available block. kInvalidIndex, if none. + }; + + struct Slab : PlacementAllocated { + // A slab is placement-allocated into an aligned pointer from a separate allocation. + // Ownership of the allocation is transferred to the slab on creation. + // | ---------- allocation --------- | + // | pad | Slab | data ------------> | + Slab(std::unique_ptr allocation, IndexLinkNode* head); + + void Splice(); + + std::unique_ptr allocation; + IndexLinkNode* freeList; + Slab* prev; + Slab* next; + Index blocksInUse; + }; + + SlabAllocatorImpl(Index blocksPerSlab, + uint32_t allocationAlignment, + uint32_t slabBlocksOffset, + uint32_t blockStride, + uint32_t indexLinkNodeOffset); + ~SlabAllocatorImpl(); + + // Allocate a new block of memory. + void* Allocate(); + + // Deallocate a block of memory. + void Deallocate(void* ptr); + + private: + // The maximum value is reserved to indicate the end of the list. + static Index kInvalidIndex; + + // Get the IndexLinkNode |offset| slots away. + IndexLinkNode* OffsetFrom(IndexLinkNode* node, std::make_signed_t offset) const; + + // Compute the pointer to the IndexLinkNode from an allocated object. + IndexLinkNode* NodeFromObject(void* object) const; + + // Compute the pointer to the object from an IndexLinkNode. + void* ObjectFromNode(IndexLinkNode* node) const; + + bool IsNodeInSlab(Slab* slab, IndexLinkNode* node) const; + + // The Slab stores a linked-list of free allocations. + // PushFront/PopFront adds/removes an allocation from the free list. + void PushFront(Slab* slab, IndexLinkNode* node) const; + IndexLinkNode* PopFront(Slab* slab) const; + + // Replace the current slab with a new one, and chain the old one off of it. + // Both slabs may still be used for for allocation/deallocation, but older slabs + // will be a little slower to get allocations from. + void GetNewSlab(); + + const uint32_t mAllocationAlignment; + + // | Slab | pad | Obj | pad | Node | pad | Obj | pad | Node | pad | .... + // | -----------| mSlabBlocksOffset + // | | ---------------------- | mBlockStride + // | | ----------| mIndexLinkNodeOffset + // | --------------------------------------> (mSlabBlocksOffset + mBlocksPerSlab * mBlockStride) + + // A Slab is metadata, followed by the aligned memory to allocate out of. |mSlabBlocksOffset| is + // the offset to the start of the aligned memory region. + const uint32_t mSlabBlocksOffset; + + // Because alignment of allocations may introduce padding, |mBlockStride| is the + // distance between aligned blocks of (Allocation + IndexLinkNode) + const uint32_t mBlockStride; + + // The IndexLinkNode is stored after the Allocation itself. This is the offset to it. + const uint32_t mIndexLinkNodeOffset; + + const Index mBlocksPerSlab; // The total number of blocks in a slab. + + const size_t mTotalAllocationSize; + + struct SentinelSlab : Slab { + SentinelSlab(); + ~SentinelSlab(); + + void Prepend(Slab* slab); + }; + + SentinelSlab mAvailableSlabs; // Available slabs to service allocations. + SentinelSlab mFullSlabs; // Full slabs. Stored here so we can skip checking them. + SentinelSlab mRecycledSlabs; // Recycled slabs. Not immediately added to |mAvailableSlabs| so + // we don't thrash the current "active" slab. +}; + +template +class SlabAllocator : public SlabAllocatorImpl { + // Helper struct for computing alignments + struct Storage { + Slab slab; + struct Block { + // If the size is unspecified, use sizeof(T) as default. Defined here and not as a + // default template parameter because T may be an incomplete type at the time of + // declaration. + static constexpr size_t kSize = ObjectSize == 0 ? sizeof(T) : ObjectSize; + static_assert(kSize >= sizeof(T), ""); + + alignas(alignof(T)) char object[kSize]; + IndexLinkNode node; + } blocks[]; + }; + + public: + SlabAllocator(Index blocksPerSlab) + : SlabAllocatorImpl( + blocksPerSlab, + alignof(Storage), // allocationAlignment + offsetof(Storage, blocks[0]), // slabBlocksOffset + offsetof(Storage, blocks[1]) - offsetof(Storage, blocks[0]), // blockStride + offsetof(typename Storage::Block, node) // indexLinkNodeOffset + ) { + } + + template + T* Allocate(Args&&... args) { + void* ptr = SlabAllocatorImpl::Allocate(); + return new (ptr) T(std::forward(args)...); + } + + void Deallocate(T* object) { + SlabAllocatorImpl::Deallocate(object); + } +}; + +#endif // COMMON_SLABALLOCATOR_H_ diff --git a/src/tests/unittests/PlacementAllocatedTests.cpp b/src/tests/unittests/PlacementAllocatedTests.cpp new file mode 100644 index 0000000000..d483d6eed6 --- /dev/null +++ b/src/tests/unittests/PlacementAllocatedTests.cpp @@ -0,0 +1,115 @@ +// Copyright 2020 The Dawn 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 + +#include "common/PlacementAllocated.h" + +using namespace testing; + +namespace { + + enum class DestructedClass { + Foo, + Bar, + }; + + class MockDestructor { + public: + MOCK_METHOD2(Call, void(void*, DestructedClass)); + }; + + std::unique_ptr> mockDestructor; + + class PlacementAllocatedTests : public Test { + void SetUp() override { + mockDestructor = std::make_unique>(); + } + + void TearDown() override { + mockDestructor = nullptr; + } + }; + + struct Foo : PlacementAllocated { + virtual ~Foo() { + mockDestructor->Call(this, DestructedClass::Foo); + } + }; + + struct Bar : Foo { + ~Bar() override { + mockDestructor->Call(this, DestructedClass::Bar); + } + }; +} // namespace + +// Test that deletion calls the destructor and does not free memory. +TEST_F(PlacementAllocatedTests, DeletionDoesNotFreeMemory) { + void* ptr = malloc(sizeof(Foo)); + + Foo* foo = new (ptr) Foo(); + + EXPECT_CALL(*mockDestructor, Call(foo, DestructedClass::Foo)); + delete foo; + + // Touch the memory, this shouldn't crash. + static_assert(sizeof(Foo) >= sizeof(uint32_t), ""); + *reinterpret_cast(foo) = 42; + + free(ptr); +} + +// Test that destructing an instance of a derived class calls the derived, then base destructor, and +// does not free memory. +TEST_F(PlacementAllocatedTests, DeletingDerivedClassCallsBaseDestructor) { + void* ptr = malloc(sizeof(Bar)); + + Bar* bar = new (ptr) Bar(); + + { + InSequence s; + EXPECT_CALL(*mockDestructor, Call(bar, DestructedClass::Bar)); + EXPECT_CALL(*mockDestructor, Call(bar, DestructedClass::Foo)); + delete bar; + } + + // Touch the memory, this shouldn't crash. + static_assert(sizeof(Bar) >= sizeof(uint32_t), ""); + *reinterpret_cast(bar) = 42; + + free(ptr); +} + +// Test that destructing an instance of a base class calls the derived, then base destructor, and +// does not free memory. +TEST_F(PlacementAllocatedTests, DeletingBaseClassCallsDerivedDestructor) { + void* ptr = malloc(sizeof(Bar)); + + Foo* foo = new (ptr) Bar(); + + { + InSequence s; + EXPECT_CALL(*mockDestructor, Call(foo, DestructedClass::Bar)); + EXPECT_CALL(*mockDestructor, Call(foo, DestructedClass::Foo)); + delete foo; + } + + // Touch the memory, this shouldn't crash. + static_assert(sizeof(Bar) >= sizeof(uint32_t), ""); + *reinterpret_cast(foo) = 42; + + free(ptr); +} diff --git a/src/tests/unittests/SlabAllocatorTests.cpp b/src/tests/unittests/SlabAllocatorTests.cpp new file mode 100644 index 0000000000..12da0109b1 --- /dev/null +++ b/src/tests/unittests/SlabAllocatorTests.cpp @@ -0,0 +1,180 @@ +// Copyright 2020 The Dawn 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 "common/Math.h" +#include "common/SlabAllocator.h" + +namespace { + + struct Foo : public PlacementAllocated { + Foo(int value) : value(value) { + } + + int value; + }; + + struct alignas(256) AlignedFoo : public Foo { + using Foo::Foo; + }; + +} // namespace + +// Test that a slab allocator of a single object works. +TEST(SlabAllocatorTests, Single) { + SlabAllocator allocator(1); + + Foo* obj = allocator.Allocate(4); + EXPECT_EQ(obj->value, 4); + + allocator.Deallocate(obj); +} + +// Allocate multiple objects and check their data is correct. +TEST(SlabAllocatorTests, AllocateSequential) { + // Check small alignment + { + SlabAllocator allocator(5); + + std::vector objects; + for (int i = 0; i < 10; ++i) { + auto* ptr = allocator.Allocate(i); + EXPECT_TRUE(std::find(objects.begin(), objects.end(), ptr) == objects.end()); + objects.push_back(ptr); + } + + for (int i = 0; i < 10; ++i) { + // Check that the value is correct and hasn't been trampled. + EXPECT_EQ(objects[i]->value, i); + + // Check that the alignment is correct. + EXPECT_TRUE(IsPtrAligned(objects[i], alignof(Foo))); + } + + // Deallocate all of the objects. + for (Foo* object : objects) { + allocator.Deallocate(object); + } + } + + // Check large alignment + { + SlabAllocator allocator(9); + + std::vector objects; + for (int i = 0; i < 21; ++i) { + auto* ptr = allocator.Allocate(i); + EXPECT_TRUE(std::find(objects.begin(), objects.end(), ptr) == objects.end()); + objects.push_back(ptr); + } + + for (int i = 0; i < 21; ++i) { + // Check that the value is correct and hasn't been trampled. + EXPECT_EQ(objects[i]->value, i); + + // Check that the alignment is correct. + EXPECT_TRUE(IsPtrAligned(objects[i], 256)); + } + + // Deallocate all of the objects. + for (AlignedFoo* object : objects) { + allocator.Deallocate(object); + } + } +} + +// Test that when reallocating a number of objects <= pool size, all memory is reused. +TEST(SlabAllocatorTests, ReusesFreedMemory) { + SlabAllocator allocator(17); + + // Allocate a number of objects. + std::set objects; + for (int i = 0; i < 17; ++i) { + EXPECT_TRUE(objects.insert(allocator.Allocate(i)).second); + } + + // Deallocate all of the objects. + for (Foo* object : objects) { + allocator.Deallocate(object); + } + + std::set reallocatedObjects; + // Allocate objects again. All of the pointers should be the same. + for (int i = 0; i < 17; ++i) { + Foo* ptr = allocator.Allocate(i); + EXPECT_TRUE(reallocatedObjects.insert(ptr).second); + EXPECT_TRUE(std::find(objects.begin(), objects.end(), ptr) != objects.end()); + } + + // Deallocate all of the objects. + for (Foo* object : objects) { + allocator.Deallocate(object); + } +} + +// Test many allocations and deallocations. Meant to catch corner cases with partially +// empty slabs. +TEST(SlabAllocatorTests, AllocateDeallocateMany) { + SlabAllocator allocator(17); + + std::set objects; + std::set set3; + std::set set7; + + // Allocate many objects. + for (uint32_t i = 0; i < 800; ++i) { + Foo* object = allocator.Allocate(i); + EXPECT_TRUE(objects.insert(object).second); + + if (i % 3 == 0) { + set3.insert(object); + } else if (i % 7 == 0) { + set7.insert(object); + } + } + + // Deallocate every 3rd object. + for (Foo* object : set3) { + allocator.Deallocate(object); + objects.erase(object); + } + + // Allocate many more objects + for (uint32_t i = 0; i < 800; ++i) { + Foo* object = allocator.Allocate(i); + EXPECT_TRUE(objects.insert(object).second); + + if (i % 7 == 0) { + set7.insert(object); + } + } + + // Deallocate every 7th object from the first and second rounds of allocation. + for (Foo* object : set7) { + allocator.Deallocate(object); + objects.erase(object); + } + + // Allocate objects again + for (uint32_t i = 0; i < 800; ++i) { + Foo* object = allocator.Allocate(i); + EXPECT_TRUE(objects.insert(object).second); + } + + // Deallocate the rest of the objects + for (Foo* object : objects) { + allocator.Deallocate(object); + } +}