diff --git a/BUILD.gn b/BUILD.gn index 45b2381ff5..78c2c04b89 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -112,6 +112,8 @@ source_set("libdawn_native_sources") { "src/dawn_native/BindGroup.h", "src/dawn_native/BindGroupLayout.cpp", "src/dawn_native/BindGroupLayout.h", + "src/dawn_native/BuddyAllocator.cpp", + "src/dawn_native/BuddyAllocator.h", "src/dawn_native/Buffer.cpp", "src/dawn_native/Buffer.h", "src/dawn_native/CommandAllocator.cpp", @@ -598,6 +600,7 @@ test("dawn_unittests") { ] sources += [ "src/tests/unittests/BitSetIteratorTests.cpp", + "src/tests/unittests/BuddyAllocatorTests.cpp", "src/tests/unittests/CommandAllocatorTests.cpp", "src/tests/unittests/EnumClassBitmasksTests.cpp", "src/tests/unittests/ErrorTests.cpp", diff --git a/src/dawn_native/BuddyAllocator.cpp b/src/dawn_native/BuddyAllocator.cpp new file mode 100644 index 0000000000..a96dd88318 --- /dev/null +++ b/src/dawn_native/BuddyAllocator.cpp @@ -0,0 +1,267 @@ +// Copyright 2019 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 "dawn_native/BuddyAllocator.h" + +#include "common/Assert.h" +#include "common/Math.h" + +namespace dawn_native { + + BuddyAllocator::BuddyAllocator(uint64_t maxSize) : mMaxBlockSize(maxSize) { + ASSERT(IsPowerOfTwo(maxSize)); + + mFreeLists.resize(Log2(mMaxBlockSize) + 1); + + // Insert the level0 free block. + mRoot = new BuddyBlock(maxSize, /*offset*/ 0); + mFreeLists[0] = {mRoot}; + } + + BuddyAllocator::~BuddyAllocator() { + if (mRoot) { + DeleteBlock(mRoot); + } + } + + uint64_t BuddyAllocator::ComputeTotalNumOfFreeBlocksForTesting() const { + return ComputeNumOfFreeBlocks(mRoot); + } + + uint64_t BuddyAllocator::ComputeNumOfFreeBlocks(BuddyBlock* block) const { + if (block->mState == BlockState::Free) { + return 1; + } else if (block->mState == BlockState::Split) { + return ComputeNumOfFreeBlocks(block->split.pLeft) + + ComputeNumOfFreeBlocks(block->split.pLeft->pBuddy); + } + return 0; + } + + uint32_t BuddyAllocator::ComputeLevelFromBlockSize(uint64_t blockSize) const { + // Every level in the buddy system can be indexed by order-n where n = log2(blockSize). + // However, mFreeList zero-indexed by level. + // For example, blockSize=4 is Level1 if MAX_BLOCK is 8. + return Log2(mMaxBlockSize) - Log2(blockSize); + } + + uint64_t BuddyAllocator::GetNextFreeAlignedBlock(size_t allocationBlockLevel, + uint64_t alignment) const { + ASSERT(IsPowerOfTwo(alignment)); + // The current level is the level that corresponds to the allocation size. The free list may + // not contain a block at that level until a larger one gets allocated (and splits). + // Continue to go up the tree until such a larger block exists. + // + // Even if the block exists at the level, it cannot be used if it's offset is unaligned. + // When the alignment is also a power-of-two, we simply use the next free block whose size + // is greater than or equal to the alignment value. + // + // After one 8-byte allocation: + // + // Level -------------------------------- + // 0 32 | S | + // -------------------------------- + // 1 16 | S | F2 | S - split + // -------------------------------- F - free + // 2 8 | Aa | F1 | | A - allocated + // -------------------------------- + // + // Allocate(size=8, alignment=8) will be satisfied by using F1. + // Allocate(size=8, alignment=4) will be satified by using F1. + // Allocate(size=8, alignment=16) will be satisified by using F2. + // + for (size_t currLevel = allocationBlockLevel; currLevel >= 0; currLevel--) { + BuddyBlock* freeBlock = mFreeLists[currLevel].head; + if (freeBlock && (freeBlock->mOffset % alignment == 0)) { + return currLevel; + } + + if (currLevel == 0) { + break; + } + } + return INVALID_OFFSET; // No free block exists at any level. + } + + // Inserts existing free block into the free-list. + // Called by allocate upon splitting to insert a child block into a free-list. + // Note: Always insert into the head of the free-list. As when a larger free block at a lower + // level was split, there were no smaller free blocks at a higher level to allocate. + void BuddyAllocator::InsertFreeBlock(BuddyBlock* block, size_t level) { + ASSERT(block->mState == BlockState::Free); + + // Inserted block is now the front (no prev). + block->free.pPrev = nullptr; + + // Old head is now the inserted block's next. + block->free.pNext = mFreeLists[level].head; + + // Block already in HEAD position (ex. right child was inserted first). + if (mFreeLists[level].head != nullptr) { + // Old head's previous is the inserted block. + mFreeLists[level].head->free.pPrev = block; + } + + mFreeLists[level].head = block; + } + + void BuddyAllocator::RemoveFreeBlock(BuddyBlock* block, size_t level) { + ASSERT(block->mState == BlockState::Free); + + if (mFreeLists[level].head == block) { + // Block is in HEAD position. + mFreeLists[level].head = mFreeLists[level].head->free.pNext; + } else { + // Block is after HEAD position. + BuddyBlock* pPrev = block->free.pPrev; + BuddyBlock* pNext = block->free.pNext; + + ASSERT(pPrev != nullptr); + ASSERT(pPrev->mState == BlockState::Free); + + pPrev->free.pNext = pNext; + + if (pNext != nullptr) { + ASSERT(pNext->mState == BlockState::Free); + pNext->free.pPrev = pPrev; + } + } + } + + uint64_t BuddyAllocator::Allocate(uint64_t allocationSize, uint64_t alignment) { + if (allocationSize == 0 || allocationSize > mMaxBlockSize) { + return INVALID_OFFSET; + } + + // Compute the level + const uint32_t allocationSizeToLevel = ComputeLevelFromBlockSize(allocationSize); + + ASSERT(allocationSizeToLevel < mFreeLists.size()); + + uint64_t currBlockLevel = GetNextFreeAlignedBlock(allocationSizeToLevel, alignment); + + // Error when no free blocks exist (allocator is full) + if (currBlockLevel == INVALID_OFFSET) { + return INVALID_OFFSET; + } + + // Split free blocks level-by-level. + // Terminate when the current block level is equal to the computed level of the requested + // allocation. + BuddyBlock* currBlock = mFreeLists[currBlockLevel].head; + + for (; currBlockLevel < allocationSizeToLevel; currBlockLevel++) { + ASSERT(currBlock->mState == BlockState::Free); + + // Remove curr block (about to be split). + RemoveFreeBlock(currBlock, currBlockLevel); + + // Create two free child blocks (the buddies). + const uint64_t nextLevelSize = currBlock->mSize / 2; + BuddyBlock* leftChildBlock = new BuddyBlock(nextLevelSize, currBlock->mOffset); + BuddyBlock* rightChildBlock = + new BuddyBlock(nextLevelSize, currBlock->mOffset + nextLevelSize); + + // Remember the parent to merge these back upon de-allocation. + rightChildBlock->pParent = currBlock; + leftChildBlock->pParent = currBlock; + + // Make them buddies. + leftChildBlock->pBuddy = rightChildBlock; + rightChildBlock->pBuddy = leftChildBlock; + + // Insert the children back into the free list into the next level. + // The free list does not require a specific order. However, an order is specified as + // it's ideal to allocate lower addresses first by having the leftmost child in HEAD. + InsertFreeBlock(rightChildBlock, currBlockLevel + 1); + InsertFreeBlock(leftChildBlock, currBlockLevel + 1); + + // Curr block is now split. + currBlock->mState = BlockState::Split; + currBlock->split.pLeft = leftChildBlock; + + // Decend down into the next level. + currBlock = leftChildBlock; + } + + // Remove curr block from free-list (now allocated). + RemoveFreeBlock(currBlock, currBlockLevel); + currBlock->mState = BlockState::Allocated; + + return currBlock->mOffset; + } + + void BuddyAllocator::Deallocate(uint64_t offset) { + BuddyBlock* curr = mRoot; + + // TODO(bryan.bernhart@intel.com): Optimize de-allocation. + // Passing allocationSize directly will avoid the following level-by-level search; + // however, it requires the size information to be stored outside the allocator. + + // Search for the free block node that corresponds to the block offset. + size_t currBlockLevel = 0; + while (curr->mState == BlockState::Split) { + if (offset < curr->split.pLeft->pBuddy->mOffset) { + curr = curr->split.pLeft; + } else { + curr = curr->split.pLeft->pBuddy; + } + + currBlockLevel++; + } + + ASSERT(curr->mState == BlockState::Allocated); + + // Ensure the block is at the correct level + ASSERT(currBlockLevel == ComputeLevelFromBlockSize(curr->mSize)); + + // Mark curr free so we can merge. + curr->mState = BlockState::Free; + + // Merge the buddies (LevelN-to-Level0). + while (currBlockLevel > 0 && curr->pBuddy->mState == BlockState::Free) { + // Remove the buddy. + RemoveFreeBlock(curr->pBuddy, currBlockLevel); + + BuddyBlock* parent = curr->pParent; + + // The buddies were inserted in a specific order but + // could be deleted in any order. + DeleteBlock(curr->pBuddy); + DeleteBlock(curr); + + // Parent is now free. + parent->mState = BlockState::Free; + + // Ascend up to the next level (parent block). + curr = parent; + currBlockLevel--; + } + + InsertFreeBlock(curr, currBlockLevel); + } + + // Helper which deletes a block in the tree recursively (post-order). + void BuddyAllocator::DeleteBlock(BuddyBlock* block) { + ASSERT(block != nullptr); + + if (block->mState == BlockState::Split) { + // Delete the pair in same order we inserted. + DeleteBlock(block->split.pLeft->pBuddy); + DeleteBlock(block->split.pLeft); + } + delete block; + } + +} // namespace dawn_native \ No newline at end of file diff --git a/src/dawn_native/BuddyAllocator.h b/src/dawn_native/BuddyAllocator.h new file mode 100644 index 0000000000..df689f3b3d --- /dev/null +++ b/src/dawn_native/BuddyAllocator.h @@ -0,0 +1,111 @@ +// Copyright 2019 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 DAWNNATIVE_BUDDYALLOCATOR_H_ +#define DAWNNATIVE_BUDDYALLOCATOR_H_ + +#include +#include + +namespace dawn_native { + + static constexpr uint64_t INVALID_OFFSET = std::numeric_limits::max(); + + // Buddy allocator uses the buddy memory allocation technique to satisify an allocation request. + // Memory is split into halves until just large enough to fit to the request. This + // requires the allocation size to be a power-of-two value. The allocator "allocates" a block by + // returning the starting offset whose size is guaranteed to be greater than or equal to the + // allocation size. To deallocate, the same offset is used to find the corresponding block. + // + // Internally, it manages a free list to track free blocks in a full binary tree. + // Every index in the free list corresponds to a level in the tree. That level also determines + // the size of the block to be used to satisfy the request. The first level (index=0) represents + // the root whose size is also called the max block size. + // + class BuddyAllocator { + public: + BuddyAllocator(uint64_t maxSize); + ~BuddyAllocator(); + + // Required methods. + uint64_t Allocate(uint64_t allocationSize, uint64_t alignment = 1); + void Deallocate(uint64_t offset); + + // For testing purposes only. + uint64_t ComputeTotalNumOfFreeBlocksForTesting() const; + + private: + uint32_t ComputeLevelFromBlockSize(uint64_t blockSize) const; + uint64_t GetNextFreeAlignedBlock(size_t allocationBlockLevel, uint64_t alignment) const; + + enum class BlockState { Free, Split, Allocated }; + + struct BuddyBlock { + BuddyBlock(uint64_t size, uint64_t offset) + : mOffset(offset), mSize(size), mState(BlockState::Free) { + free.pPrev = nullptr; + free.pNext = nullptr; + } + + uint64_t mOffset; + uint64_t mSize; + + // Pointer to this block's buddy, iff parent is split. + // Used to quickly merge buddy blocks upon de-allocate. + BuddyBlock* pBuddy = nullptr; + BuddyBlock* pParent = nullptr; + + // Track whether this block has been split or not. + BlockState mState; + + union { + // Used upon allocation. + // Avoids searching for the next free block. + struct { + BuddyBlock* pPrev; + BuddyBlock* pNext; + } free; + + // Used upon de-allocation. + // Had this block split upon allocation, it and it's buddy is to be deleted. + struct { + BuddyBlock* pLeft; + } split; + }; + }; + + void InsertFreeBlock(BuddyBlock* block, size_t level); + void RemoveFreeBlock(BuddyBlock* block, size_t level); + void DeleteBlock(BuddyBlock* block); + + uint64_t ComputeNumOfFreeBlocks(BuddyBlock* block) const; + + // Keep track the head and tail (for faster insertion/removal). + struct BlockList { + BuddyBlock* head = nullptr; // First free block in level. + // TODO(bryan.bernhart@intel.com): Track the tail. + }; + + BuddyBlock* mRoot = nullptr; // Used to deallocate non-free blocks. + + uint64_t mMaxBlockSize = 0; + + // List of linked-lists of free blocks where the index is a level that + // corresponds to a power-of-two sized block. + std::vector mFreeLists; + }; + +} // namespace dawn_native + +#endif // DAWNNATIVE_BUDDYALLOCATOR_H_ \ No newline at end of file diff --git a/src/tests/unittests/BuddyAllocatorTests.cpp b/src/tests/unittests/BuddyAllocatorTests.cpp new file mode 100644 index 0000000000..6ef6fceded --- /dev/null +++ b/src/tests/unittests/BuddyAllocatorTests.cpp @@ -0,0 +1,325 @@ +// Copyright 2019 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 "dawn_native/BuddyAllocator.h" + +using namespace dawn_native; + +// Verify the buddy allocator with a basic test. +TEST(BuddyAllocatorTests, SingleBlock) { + // After one 32 byte allocation: + // + // Level -------------------------------- + // 0 32 | A | + // -------------------------------- + // + constexpr uint64_t maxBlockSize = 32; + BuddyAllocator allocator(maxBlockSize); + + // Check that we cannot allocate a oversized block. + ASSERT_EQ(allocator.Allocate(maxBlockSize * 2), INVALID_OFFSET); + + // Check that we cannot allocate a zero sized block. + ASSERT_EQ(allocator.Allocate(0u), INVALID_OFFSET); + + // Allocate the block. + uint64_t blockOffset = allocator.Allocate(maxBlockSize); + ASSERT_EQ(blockOffset, 0u); + + // Check that we are full. + ASSERT_EQ(allocator.Allocate(maxBlockSize), INVALID_OFFSET); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 0u); + + // Deallocate the block. + allocator.Deallocate(blockOffset); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); +} + +// Verify multiple allocations succeeds using a buddy allocator. +TEST(BuddyAllocatorTests, MultipleBlocks) { + // Fill every level in the allocator (order-n = 2^n) + const uint64_t maxBlockSize = (1ull << 16); + for (uint64_t order = 1; (1ull << order) <= maxBlockSize; order++) { + BuddyAllocator allocator(maxBlockSize); + + uint64_t blockSize = (1ull << order); + for (uint32_t blocki = 0; blocki < (maxBlockSize / blockSize); blocki++) { + ASSERT_EQ(allocator.Allocate(blockSize), blockSize * blocki); + } + } +} + +// Verify that a single allocation succeeds using a buddy allocator. +TEST(BuddyAllocatorTests, SingleSplitBlock) { + // After one 8 byte allocation: + // + // Level -------------------------------- + // 0 32 | S | + // -------------------------------- + // 1 16 | S | F | S - split + // -------------------------------- F - free + // 2 8 | A | F | | | A - allocated + // -------------------------------- + // + constexpr uint64_t maxBlockSize = 32; + BuddyAllocator allocator(maxBlockSize); + + // Allocate block (splits two blocks). + uint64_t blockOffset = allocator.Allocate(8); + ASSERT_EQ(blockOffset, 0u); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Deallocate block (merges two blocks). + allocator.Deallocate(blockOffset); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Check that we cannot allocate a block that is oversized. + ASSERT_EQ(allocator.Allocate(maxBlockSize * 2), INVALID_OFFSET); + + // Re-allocate the largest block allowed after merging. + blockOffset = allocator.Allocate(maxBlockSize); + ASSERT_EQ(blockOffset, 0u); + + allocator.Deallocate(blockOffset); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); +} + +// Verify that a multiple allocated blocks can be removed in the free-list. +TEST(BuddyAllocatorTests, MultipleSplitBlocks) { + // After four 16 byte allocations: + // + // Level -------------------------------- + // 0 32 | S | + // -------------------------------- + // 1 16 | S | S | S - split + // -------------------------------- F - free + // 2 8 | Aa | Ab | Ac | Ad | A - allocated + // -------------------------------- + // + constexpr uint64_t maxBlockSize = 32; + BuddyAllocator allocator(maxBlockSize); + + // Populates the free-list with four blocks at Level2. + + // Allocate "a" block (two splits). + constexpr uint64_t blockSizeInBytes = 8; + uint64_t blockOffsetA = allocator.Allocate(blockSizeInBytes); + ASSERT_EQ(blockOffsetA, 0u); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Allocate "b" block. + uint64_t blockOffsetB = allocator.Allocate(blockSizeInBytes); + ASSERT_EQ(blockOffsetB, blockSizeInBytes); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Allocate "c" block (three splits). + uint64_t blockOffsetC = allocator.Allocate(blockSizeInBytes); + ASSERT_EQ(blockOffsetC, blockOffsetB + blockSizeInBytes); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Allocate "d" block. + uint64_t blockOffsetD = allocator.Allocate(blockSizeInBytes); + ASSERT_EQ(blockOffsetD, blockOffsetC + blockSizeInBytes); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 0u); + + // Deallocate "d" block. + // FreeList[Level2] = [BlockD] -> x + allocator.Deallocate(blockOffsetD); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Deallocate "b" block. + // FreeList[Level2] = [BlockB] -> [BlockD] -> x + allocator.Deallocate(blockOffsetB); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Deallocate "c" block (one merges). + // FreeList[Level1] = [BlockCD] -> x + // FreeList[Level2] = [BlockB] -> x + allocator.Deallocate(blockOffsetC); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Deallocate "a" block (two merges). + // FreeList[Level0] = [BlockABCD] -> x + allocator.Deallocate(blockOffsetA); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); +} + +// Verify the buddy allocator can handle allocations of various sizes. +TEST(BuddyAllocatorTests, MultipleSplitBlockIncreasingSize) { + // After four Level4-to-Level1 byte then one L4 block allocations: + // + // Level ----------------------------------------------------------------- + // 0 512 | S | + // ----------------------------------------------------------------- + // 1 256 | S | A | + // ----------------------------------------------------------------- + // 2 128 | S | A | | | + // ----------------------------------------------------------------- + // 3 64 | S | A | | | | | | | + // ----------------------------------------------------------------- + // 4 32 | A | F | | | | | | | | | | | | | | | + // ----------------------------------------------------------------- + // + constexpr uint64_t maxBlockSize = 512; + BuddyAllocator allocator(maxBlockSize); + + ASSERT_EQ(allocator.Allocate(32), 0ull); + ASSERT_EQ(allocator.Allocate(64), 64ull); + ASSERT_EQ(allocator.Allocate(128), 128ull); + ASSERT_EQ(allocator.Allocate(256), 256ull); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Fill in the last free block. + ASSERT_EQ(allocator.Allocate(32), 32ull); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 0u); + + // Check if we're full. + ASSERT_EQ(allocator.Allocate(32), INVALID_OFFSET); +} + +// Verify very small allocations using a larger allocator works correctly. +TEST(BuddyAllocatorTests, MultipleSplitBlocksVariableSizes) { + // After allocating four pairs of one 64 byte block and one 32 byte block. + // + // Level ----------------------------------------------------------------- + // 0 512 | S | + // ----------------------------------------------------------------- + // 1 256 | S | S | + // ----------------------------------------------------------------- + // 2 128 | S | S | S | F | + // ----------------------------------------------------------------- + // 3 64 | A | S | A | A | S | A | | | + // ----------------------------------------------------------------- + // 4 32 | | | A | A | | | | | A | A | | | | | | | + // ----------------------------------------------------------------- + // + constexpr uint64_t maxBlockSize = 512; + BuddyAllocator allocator(maxBlockSize); + + ASSERT_EQ(allocator.Allocate(64), 0ull); + ASSERT_EQ(allocator.Allocate(32), 64ull); + + ASSERT_EQ(allocator.Allocate(64), 128ull); + ASSERT_EQ(allocator.Allocate(32), 96ull); + + ASSERT_EQ(allocator.Allocate(64), 192ull); + ASSERT_EQ(allocator.Allocate(32), 256ull); + + ASSERT_EQ(allocator.Allocate(64), 320ull); + ASSERT_EQ(allocator.Allocate(32), 288ull); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); +} + +// Verify the buddy allocator can deal with bad fragmentation. +TEST(BuddyAllocatorTests, MultipleSplitBlocksInterleaved) { + // Allocate every leaf then de-allocate every other of those allocations. + // + // Level ----------------------------------------------------------------- + // 0 512 | S | + // ----------------------------------------------------------------- + // 1 256 | S | S | + // ----------------------------------------------------------------- + // 2 128 | S | S | S | S | + // ----------------------------------------------------------------- + // 3 64 | S | S | S | S | S | S | S | S | + // ----------------------------------------------------------------- + // 4 32 | A | F | A | F | A | F | A | F | A | F | A | F | A | F | A | F | + // ----------------------------------------------------------------- + // + constexpr uint64_t maxBlockSize = 512; + BuddyAllocator allocator(maxBlockSize); + + // Allocate leaf blocks + constexpr uint64_t minBlockSizeInBytes = 32; + std::vector blockOffsets; + for (uint64_t i = 0; i < maxBlockSize / minBlockSizeInBytes; i++) { + blockOffsets.push_back(allocator.Allocate(minBlockSizeInBytes)); + } + + // Free every other leaf block. + for (size_t count = 1; count < blockOffsets.size(); count += 2) { + allocator.Deallocate(blockOffsets[count]); + } + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 8u); +} + +// Verify the buddy allocator can deal with multiple allocations with mixed alignments. +TEST(BuddyAllocatorTests, SameSizeVariousAlignment) { + // After two 8 byte allocations with 16 byte alignment then one 8 byte allocation with 8 byte + // alignment. + // + // Level -------------------------------- + // 0 32 | S | + // -------------------------------- + // 1 16 | S | S | S - split + // -------------------------------- F - free + // 2 8 | Aa | F | Ab | Ac | A - allocated + // -------------------------------- + // + BuddyAllocator allocator(32); + + // Allocate Aa (two splits). + ASSERT_EQ(allocator.Allocate(8, 16), 0u); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Allocate Ab (skip Aa buddy due to alignment and perform another split). + ASSERT_EQ(allocator.Allocate(8, 16), 16u); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Check that we cannot fit another. + ASSERT_EQ(allocator.Allocate(8, 16), INVALID_OFFSET); + + // Allocate Ac (zero splits and Ab's buddy is now the first free block). + ASSERT_EQ(allocator.Allocate(8, 8), 24u); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); +} + +// Verify the buddy allocator can deal with multiple allocations with equal alignments. +TEST(BuddyAllocatorTests, VariousSizeSameAlignment) { + // After two 8 byte allocations with 4 byte alignment then one 16 byte allocation with 4 byte + // alignment. + // + // Level -------------------------------- + // 0 32 | S | + // -------------------------------- + // 1 16 | S | Ac | S - split + // -------------------------------- F - free + // 2 8 | Aa | Ab | | A - allocated + // -------------------------------- + // + constexpr uint64_t maxBlockSize = 32; + constexpr uint64_t alignment = 4; + BuddyAllocator allocator(maxBlockSize); + + // Allocate block Aa (two splits) + ASSERT_EQ(allocator.Allocate(8, alignment), 0u); + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 2u); + + // Allocate block Ab (Aa's buddy) + ASSERT_EQ(allocator.Allocate(8, alignment), 8u); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 1u); + + // Check that we can still allocate Ac. + ASSERT_EQ(allocator.Allocate(16, alignment), 16ull); + + ASSERT_EQ(allocator.ComputeTotalNumOfFreeBlocksForTesting(), 0u); +} \ No newline at end of file