Browse Source

Add a new allocator based on segragated lists

Panagiotis Christopoulos Charitos 3 years ago
parent
commit
f819c560f0

+ 2 - 2
AnKi/Renderer/Renderer.cpp

@@ -57,7 +57,7 @@ static Vec2 generateJitter(U32 frame)
 	F32 fraction = invBase;
 	while(index > 0)
 	{
-		result.x() += (index % baseX) * fraction;
+		result.x() += F32(index % baseX) * fraction;
 		index /= baseX;
 		fraction *= invBase;
 	}
@@ -68,7 +68,7 @@ static Vec2 generateJitter(U32 frame)
 	fraction = invBase;
 	while(index > 0)
 	{
-		result.y() += (index % baseY) * fraction;
+		result.y() += F32(index % baseY) * fraction;
 		index /= baseY;
 		fraction *= invBase;
 	}

+ 1 - 0
AnKi/Util.h

@@ -44,6 +44,7 @@
 #include <AnKi/Util/BuddyAllocatorBuilder.h>
 #include <AnKi/Util/StackAllocatorBuilder.h>
 #include <AnKi/Util/ClassAllocatorBuilder.h>
+#include <AnKi/Util/SegregatedListsAllocatorBuilder.h>
 
 /// @defgroup util Utilities (like STL)
 

+ 2 - 1
AnKi/Util/Functions.cpp

@@ -9,7 +9,8 @@
 
 namespace anki {
 
-thread_local std::mt19937_64 g_randromGenerator(U64(HighRezTimer::getCurrentTime() * 1000000.0));
+static std::random_device g_rd;
+thread_local static std::mt19937_64 g_randromGenerator(g_rd());
 
 U64 getRandom()
 {

+ 1 - 1
AnKi/Util/HighRezTimerPosix.cpp

@@ -66,7 +66,7 @@ void HighRezTimer::sleep(Second sec)
 Second HighRezTimer::getCurrentTime()
 {
 	// Second(ticks) / 1000.0
-	return static_cast<Second>(getNs()) * 1e-9;
+	return Second(getNs()) * 1e-9;
 }
 
 } // end namespace anki

+ 1 - 1
AnKi/Util/Memory.cpp

@@ -97,7 +97,7 @@ void* mallocAligned(PtrSize size, PtrSize alignmentBytes)
 	}
 	else
 	{
-		ANKI_UTIL_LOGE("_aligned_malloc() failed");
+		ANKI_UTIL_LOGE("_aligned_malloc() failed. Size %zu, alignment %zu", size, alignmentBytes);
 	}
 
 	return out;

+ 124 - 0
AnKi/Util/SegregatedListsAllocatorBuilder.h

@@ -0,0 +1,124 @@
+// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#pragma once
+
+#include <AnKi/Util/Array.h>
+#include <AnKi/Util/DynamicArray.h>
+#include <AnKi/Util/StringList.h>
+
+namespace anki {
+
+/// @addtogroup util_memory
+/// @{
+
+namespace detail {
+
+/// Free block.
+/// @memberof SegregatedListsAllocatorBuilder
+/// @internal
+class SegregatedListsAllocatorBuilderFreeBlock
+{
+public:
+	PtrSize m_size = 0;
+	PtrSize m_address = 0;
+};
+
+} // end namespace detail
+
+/// The base class for all user memory chunks of SegregatedListsAllocatorBuilder.
+/// @memberof SegregatedListsAllocatorBuilder
+template<typename TChunk, U32 T_CLASS_COUNT>
+class SegregatedListsAllocatorBuilderChunkBase
+{
+	template<typename TChunk_, typename TInterface, typename TLock>
+	friend class SegregatedListsAllocatorBuilder;
+
+private:
+	Array<DynamicArray<detail::SegregatedListsAllocatorBuilderFreeBlock>, T_CLASS_COUNT> m_freeLists;
+	PtrSize m_totalSize = 0;
+	PtrSize m_freeSize = 0;
+};
+
+/// It provides the tools to build allocators base on segregated lists and best fit.
+/// @tparam TChunk A user defined class that contains a memory chunk. It should inherit from
+///                SegregatedListsAllocatorBuilderChunkBase.
+/// @tparam TInterface The interface that contains the following members:
+///                    @code
+///                    /// The number of classes
+///                    static constexpr U32 getClassCount();
+///                    /// Max size for each class.
+///                    void getClassInfo(U32 idx, PtrSize& size) const;
+///                    /// Allocates a new user defined chunk of memory.
+///                    Error allocateChunk(TChunk*& newChunk, PtrSize& chunkSize);
+///                    /// Deletes a chunk.
+///                    void deleteChunk(TChunk* chunk);
+///                    /// Get an allocator for internal allocations of the builder.
+///                    SomeAllocator& getAllocator();
+///                    const SomeAllocator getAllocator() const;
+///                    @endcode
+/// @tparam TLock User defined lock (eg Mutex).
+template<typename TChunk, typename TInterface, typename TLock>
+class SegregatedListsAllocatorBuilder
+{
+public:
+	SegregatedListsAllocatorBuilder() = default;
+
+	~SegregatedListsAllocatorBuilder();
+
+	SegregatedListsAllocatorBuilder(const SegregatedListsAllocatorBuilder&) = delete;
+
+	SegregatedListsAllocatorBuilder& operator=(const SegregatedListsAllocatorBuilder&) = delete;
+
+	/// Allocate memory.
+	/// @param size The size to allocate.
+	/// @param alignment The alignment of the returned address.
+	/// @param[out] chunk The chunk that the memory belongs to.
+	/// @param[out] offset The offset inside the chunk.
+	/// @note This is thread safe.
+	Error allocate(PtrSize size, PtrSize alignment, TChunk*& chunk, PtrSize& offset);
+
+	/// Free memory.
+	/// @param chunk The chunk the allocation belongs to.
+	/// @param offset The memory offset inside the chunk.
+	void free(TChunk* chunk, PtrSize offset, PtrSize size);
+
+	/// Validate the internal structures. It's only used in testing.
+	Error validate() const;
+
+	/// Print debug info.
+	void printFreeBlocks(StringListAuto& strList) const;
+
+	/// It's 1-(largestBlockOfFreeMemory/totalFreeMemory). 0.0 is no fragmentation, 1.0 is totally fragmented.
+	[[nodiscard]] F32 computeExternalFragmentation(PtrSize baseSize = 1) const;
+
+	/// Adam Sawicki metric. 0.0 is no fragmentation, 1.0 is totally fragmented.
+	[[nodiscard]] F32 computeExternalFragmentationSawicki(PtrSize baseSize = 1) const;
+
+private:
+	using FreeBlock = detail::SegregatedListsAllocatorBuilderFreeBlock;
+	using ChunksIterator = typename DynamicArray<TChunk*>::Iterator;
+
+	TInterface m_interface; ///< XXX
+
+	DynamicArray<TChunk*> m_chunks;
+
+	mutable TLock m_lock;
+
+	U32 findClass(PtrSize size, PtrSize alignment) const;
+
+	/// Choose the best free block out of 2 given the allocation size and alignment.
+	static Bool chooseBestFit(PtrSize allocSize, PtrSize allocAlignment, FreeBlock* blockA, FreeBlock* blockB,
+							  FreeBlock*& bestBlock);
+
+	/// Place a free block in one of the lists.
+	/// @param[in,out] chunk The input chunk. If it's freed the pointer will become null.
+	void placeFreeBlock(PtrSize address, PtrSize size, ChunksIterator chunkIt);
+};
+/// @}
+
+} // end namespace anki
+
+#include <AnKi/Util/SegregatedListsAllocatorBuilder.inl.h>

+ 611 - 0
AnKi/Util/SegregatedListsAllocatorBuilder.inl.h

@@ -0,0 +1,611 @@
+// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <AnKi/Util/SegregatedListsAllocatorBuilder.h>
+
+namespace anki {
+
+template<typename TChunk, typename TInterface, typename TLock>
+SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::~SegregatedListsAllocatorBuilder()
+{
+	ANKI_ASSERT(m_chunks.isEmpty() && "Forgot to free memory");
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+U32 SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::findClass(PtrSize size, PtrSize alignment) const
+{
+	ANKI_ASSERT(size > 0 && alignment > 0);
+
+	for(U32 i = 0; i < TInterface::getClassCount(); ++i)
+	{
+		PtrSize maxSize;
+		m_interface.getClassInfo(i, maxSize);
+
+		if(size <= maxSize)
+		{
+			if(alignment <= maxSize)
+			{
+				// Found the class
+				return i;
+			}
+			else
+			{
+				// The class found doesn't have the proper alignment. Need to go higher
+			}
+		}
+	}
+
+	// Not found
+	return MAX_U32;
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+Bool SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::chooseBestFit(PtrSize allocSize,
+																			   PtrSize allocAlignment,
+																			   FreeBlock* blockA, FreeBlock* blockB,
+																			   FreeBlock*& bestBlock)
+{
+	ANKI_ASSERT(allocSize > 0 && allocAlignment > 0);
+	ANKI_ASSERT(blockA || blockB);
+
+	bestBlock = nullptr;
+
+	PtrSize remainingSizeA = 0;
+	if(blockA)
+	{
+		const PtrSize alignedAddress = getAlignedRoundUp(allocAlignment, blockA->m_address);
+		const Bool fits = alignedAddress + allocSize <= blockA->m_address + blockA->m_size;
+
+		if(fits)
+		{
+			bestBlock = blockA;
+			remainingSizeA = blockA->m_size - allocSize - (alignedAddress - blockA->m_address);
+		}
+	}
+
+	if(blockB)
+	{
+		const PtrSize alignedAddress = getAlignedRoundUp(allocAlignment, blockB->m_address);
+		const Bool fits = alignedAddress + allocSize <= blockB->m_address + blockB->m_size;
+
+		if(fits)
+		{
+			if(bestBlock)
+			{
+				const PtrSize remainingSizeB = blockB->m_size - allocSize - (alignedAddress - blockB->m_address);
+
+				if(remainingSizeB < remainingSizeA)
+				{
+					bestBlock = blockB;
+				}
+			}
+			else
+			{
+				bestBlock = blockB;
+			}
+		}
+	}
+
+	if(bestBlock)
+	{
+		return allocSize == bestBlock->m_size;
+	}
+	else
+	{
+		return false;
+	}
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+void SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::placeFreeBlock(PtrSize address, PtrSize size,
+																				ChunksIterator chunkIt)
+{
+	ANKI_ASSERT(size > 0 && isAligned(m_interface.getMinSizeAlignment(), size));
+	ANKI_ASSERT(chunkIt != m_chunks.getEnd());
+
+	TChunk& chunk = *(*chunkIt);
+	ANKI_ASSERT(address + size <= chunk.m_totalSize);
+
+	// First search everything to find other coalesced free blocks
+	U32 leftBlock = MAX_U32;
+	U32 leftClass = MAX_U32;
+	U32 rightBlock = MAX_U32;
+	U32 rightClass = MAX_U32;
+	for(U32 classIdx = 0; classIdx < TInterface::getClassCount(); ++classIdx)
+	{
+		const DynamicArray<FreeBlock>& freeLists = chunk.m_freeLists[classIdx];
+
+		if(freeLists.getSize() == 0)
+		{
+			continue;
+		}
+
+		FreeBlock newBlock;
+		newBlock.m_address = address;
+		newBlock.m_size = size;
+		auto it = std::lower_bound(freeLists.getBegin(), freeLists.getEnd(), newBlock,
+								   [](const FreeBlock& a, const FreeBlock& b) {
+									   return a.m_address + a.m_size < b.m_address;
+								   });
+
+		if(it == freeLists.getEnd())
+		{
+			continue;
+		}
+
+		const U32 pos = U32(it - freeLists.getBegin());
+		const FreeBlock& freeBlock = freeLists[pos];
+
+		if(freeBlock.m_address + freeBlock.m_size == address)
+		{
+			// Free block is in the left of the new
+
+			ANKI_ASSERT(leftBlock == MAX_U32);
+			leftBlock = pos;
+			leftClass = classIdx;
+
+			if(pos + 1 < freeLists.getSize())
+			{
+				const FreeBlock& freeBlock2 = freeLists[pos + 1];
+
+				if(address + size == freeBlock2.m_address)
+				{
+					ANKI_ASSERT(rightBlock == MAX_U32);
+					rightBlock = pos + 1;
+					rightClass = classIdx;
+				}
+			}
+		}
+		else if(address + size == freeBlock.m_address)
+		{
+			// Free block is in the right of the new
+
+			ANKI_ASSERT(rightBlock == MAX_U32);
+			rightBlock = pos;
+			rightClass = classIdx;
+
+			if(pos > 0)
+			{
+				const FreeBlock& freeBlock2 = freeLists[pos - 1];
+
+				if(freeBlock2.m_address + freeBlock2.m_size == address)
+				{
+					ANKI_ASSERT(leftBlock == MAX_U32);
+					leftBlock = pos - 1;
+					leftClass = classIdx;
+				}
+			}
+		}
+		else
+		{
+			// Do nothing
+		}
+
+		if(leftBlock != MAX_U32 && rightBlock != MAX_U32)
+		{
+			break;
+		}
+	}
+
+	// Merge the new block
+	FreeBlock newBlock;
+	newBlock.m_address = address;
+	newBlock.m_size = size;
+
+	if(leftBlock != MAX_U32)
+	{
+		const FreeBlock& lblock = chunk.m_freeLists[leftClass][leftBlock];
+
+		newBlock.m_address = lblock.m_address;
+		newBlock.m_size += lblock.m_size;
+
+		chunk.m_freeLists[leftClass].erase(m_interface.getAllocator(),
+										   chunk.m_freeLists[leftClass].getBegin() + leftBlock);
+
+		if(rightBlock != MAX_U32 && rightClass == leftClass)
+		{
+			// Both right and left blocks live in the same dynamic array. Due to the erase() above the rights's index
+			// is no longer valid, adjust it
+			ANKI_ASSERT(rightBlock > leftBlock);
+			--rightBlock;
+		}
+	}
+
+	if(rightBlock != MAX_U32)
+	{
+		const FreeBlock& rblock = chunk.m_freeLists[rightClass][rightBlock];
+
+		newBlock.m_size += rblock.m_size;
+
+		chunk.m_freeLists[rightClass].erase(m_interface.getAllocator(),
+											chunk.m_freeLists[rightClass].getBegin() + rightBlock);
+	}
+
+	// Store the new block
+	const U32 newClassIdx = findClass(newBlock.m_size, 1);
+	chunk.m_freeLists[newClassIdx].emplaceBack(m_interface.getAllocator(), newBlock);
+
+	std::sort(chunk.m_freeLists[newClassIdx].getBegin(), chunk.m_freeLists[newClassIdx].getEnd(),
+			  [](const FreeBlock& a, const FreeBlock& b) {
+				  return a.m_address < b.m_address;
+			  });
+
+	// Adjust chunk free size
+	chunk.m_freeSize += size;
+	ANKI_ASSERT(chunk.m_freeSize <= chunk.m_totalSize);
+	if(chunk.m_freeSize == chunk.m_totalSize)
+	{
+		// Chunk completely free, delete it
+
+		U32 blockCount = 0;
+		for(U32 classIdx = 0; classIdx < TInterface::getClassCount(); ++classIdx)
+		{
+			blockCount += chunk.m_freeLists[classIdx].getSize();
+			chunk.m_freeLists[classIdx].destroy(m_interface.getAllocator());
+		}
+
+		ANKI_ASSERT(blockCount == 1);
+
+		m_chunks.erase(m_interface.getAllocator(), chunkIt);
+		m_interface.deleteChunk(&chunk);
+	}
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+Error SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::allocate(PtrSize origSize, PtrSize origAlignment,
+																		   TChunk*& outChunk, PtrSize& outOffset)
+{
+	ANKI_ASSERT(origSize > 0 && origAlignment > 0);
+	const PtrSize size = getAlignedRoundUp(m_interface.getMinSizeAlignment(), origSize);
+	const PtrSize alignment = max<PtrSize>(m_interface.getMinSizeAlignment(), origAlignment);
+
+	// Find starting class
+	const U32 startingClassIdx = findClass(size, alignment);
+	if(ANKI_UNLIKELY(startingClassIdx == MAX_U32))
+	{
+		ANKI_UTIL_LOGE("Couldn't find class for allocation of size %zu", origSize);
+		return Error::OUT_OF_MEMORY;
+	}
+
+	LockGuard<TLock> lock(m_lock);
+
+	// Sort the chunks because we want to try allocate from the least empty first
+	std::sort(m_chunks.getBegin(), m_chunks.getEnd(), [](const TChunk* a, const TChunk* b) {
+		return a->m_freeSize < b->m_freeSize;
+	});
+
+	// Find a free block, its class and its chunk
+	FreeBlock* freeBlock = nullptr;
+	ChunksIterator chunkIt = m_chunks.getBegin();
+	U32 classIdx = MAX_U32;
+	for(; chunkIt != m_chunks.getEnd(); ++chunkIt)
+	{
+		classIdx = startingClassIdx;
+
+		while(classIdx < TInterface::getClassCount())
+		{
+			// Find the best fit
+			for(FreeBlock& block : (*chunkIt)->m_freeLists[classIdx])
+			{
+				const Bool theBestBlock = chooseBestFit(size, alignment, freeBlock, &block, freeBlock);
+				if(theBestBlock)
+				{
+					break;
+				}
+			}
+
+			if(freeBlock)
+			{
+				// Done
+				break;
+			}
+			else
+			{
+				// Check for free blocks in the next class
+				++classIdx;
+			}
+		}
+
+		if(freeBlock)
+		{
+			break;
+		}
+	}
+
+	if(freeBlock == nullptr)
+	{
+		// No free blocks, allocate new chunk
+
+		// Init the new chunk
+		PtrSize chunkSize;
+		TChunk* chunk;
+		ANKI_CHECK(m_interface.allocateChunk(chunk, chunkSize));
+
+		if(chunkSize < size)
+		{
+			ANKI_UTIL_LOGE("Chunk allocated can't fit the current allocation of %zu", origSize);
+			m_interface.deleteChunk(chunk);
+			return Error::OUT_OF_MEMORY;
+		}
+
+		chunk->m_totalSize = chunkSize;
+		chunk->m_freeSize = 0;
+		m_chunks.emplaceBack(m_interface.getAllocator(), chunk);
+
+		placeFreeBlock(size, chunkSize - size, m_chunks.getBegin() + m_chunks.getSize() - 1);
+
+		// Allocate
+		outChunk = chunk;
+		outOffset = 0;
+	}
+	else
+	{
+		// Have a free block, allocate from it
+
+		ANKI_ASSERT(chunkIt != m_chunks.getEnd() && freeBlock);
+
+		TChunk& chunk = *(*chunkIt);
+
+		const FreeBlock fBlock = *freeBlock;
+		chunk.m_freeLists[classIdx].erase(m_interface.getAllocator(), freeBlock);
+		freeBlock = nullptr;
+
+		ANKI_ASSERT(chunk.m_freeSize >= fBlock.m_size);
+		chunk.m_freeSize -= fBlock.m_size;
+
+		const PtrSize alignedAddress = getAlignedRoundUp(alignment, fBlock.m_address);
+
+		if(alignedAddress != fBlock.m_address)
+		{
+			// Add a free block because of alignment missmatch
+			placeFreeBlock(fBlock.m_address, alignedAddress - fBlock.m_address, chunkIt);
+		}
+
+		const PtrSize allocationEnd = alignedAddress + size;
+		const PtrSize freeBlockEnd = fBlock.m_address + fBlock.m_size;
+		if(allocationEnd < freeBlockEnd)
+		{
+			// Add what remains
+			placeFreeBlock(allocationEnd, freeBlockEnd - allocationEnd, chunkIt);
+		}
+
+		// Allocate
+		outChunk = &chunk;
+		outOffset = alignedAddress;
+	}
+
+	ANKI_ASSERT(outChunk);
+	ANKI_ASSERT(isAligned(alignment, outOffset));
+	return Error::NONE;
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+void SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::free(TChunk* chunk, PtrSize offset, PtrSize size)
+{
+	ANKI_ASSERT(chunk && size);
+
+	LockGuard<TLock> lock(m_lock);
+
+	ChunksIterator it = m_chunks.getBegin();
+	for(; it != m_chunks.getEnd(); ++it)
+	{
+		if(*it == chunk)
+		{
+			break;
+		}
+	}
+	ANKI_ASSERT(it != m_chunks.getEnd());
+
+	size = getAlignedRoundUp(m_interface.getMinSizeAlignment(), size);
+
+	placeFreeBlock(offset, size, it);
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+Error SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::validate() const
+{
+#define ANKI_SLAB_ASSERT(x, ...) \
+	do \
+	{ \
+		if(ANKI_UNLIKELY(!(x))) \
+		{ \
+			ANKI_UTIL_LOGE(__VA_ARGS__); \
+			ANKI_DEBUG_BREAK(); \
+			return Error::FUNCTION_FAILED; \
+		} \
+	} while(0)
+
+	LockGuard<TLock> lock(m_lock);
+
+	U32 chunkIdx = 0;
+	for(const TChunk* chunk : m_chunks)
+	{
+		ANKI_SLAB_ASSERT(chunk->m_totalSize > 0, "Chunk %u: Total size can't be 0", chunkIdx);
+		ANKI_SLAB_ASSERT(chunk->m_freeSize < chunk->m_totalSize,
+						 "Chunk %u: Free size (%zu) should be less than total size (%zu)", chunkIdx, chunk->m_freeSize,
+						 chunk->m_totalSize);
+
+		PtrSize freeSize = 0;
+		for(U32 c = 0; c < TInterface::getClassCount(); ++c)
+		{
+			for(U32 i = 0; i < chunk->m_freeLists[c].getSize(); ++i)
+			{
+				const FreeBlock& crnt = chunk->m_freeLists[c][i];
+				ANKI_SLAB_ASSERT(crnt.m_size > 0, "Chunk %u class %u block %u: Block size can't be 0", chunkIdx, c, i);
+				freeSize += crnt.m_size;
+
+				const U32 classIdx = findClass(crnt.m_size, 1);
+				ANKI_SLAB_ASSERT(classIdx == c, "Chunk %u class %u block %u: Free block not in the correct class",
+								 chunkIdx, c, i);
+
+				if(i > 0)
+				{
+					const FreeBlock& prev = chunk->m_freeLists[c][i - 1];
+					ANKI_SLAB_ASSERT(prev.m_address < crnt.m_address,
+									 "Chunk %u class %u block %u: Previous block should have lower address", chunkIdx,
+									 c, i);
+					ANKI_SLAB_ASSERT(prev.m_address + prev.m_size < crnt.m_address,
+									 "Chunk %u class %u block %u: Block overlaps with previous", chunkIdx, c, i);
+				}
+			}
+		}
+
+		ANKI_SLAB_ASSERT(freeSize == chunk->m_freeSize,
+						 "Chunk %u: Free size calculated doesn't match chunk's free size", chunkIdx);
+		++chunkIdx;
+	}
+
+	// Now do an expensive check that blocks never overlap with other blocks of other classes
+	chunkIdx = 0;
+	for(const TChunk* chunk : m_chunks)
+	{
+		for(U32 c = 0; c < TInterface::getClassCount(); ++c)
+		{
+			for(U32 i = 0; i < chunk->m_freeLists[c].getSize(); ++i)
+			{
+				const FreeBlock& crnt = chunk->m_freeLists[c][i];
+
+				for(U32 c2 = 0; c2 < TInterface::getClassCount(); ++c2)
+				{
+					for(U32 j = 0; j < chunk->m_freeLists[c2].getSize(); ++j)
+					{
+						if(c == c2 && i == j)
+						{
+							// Skip "crnt"
+							continue;
+						}
+
+						const FreeBlock& other = chunk->m_freeLists[c2][j];
+
+						// Check coalescing
+						if(crnt.m_address < other.m_address)
+						{
+							ANKI_SLAB_ASSERT(crnt.m_address + crnt.m_size < other.m_address,
+											 "Chunk %u class %u block %u: Block overlaps or should have been merged "
+											 "with block: class %u block %u",
+											 chunkIdx, c, i, c2, j);
+						}
+						else if(crnt.m_address > other.m_address)
+						{
+							ANKI_SLAB_ASSERT(other.m_address + other.m_size < crnt.m_address,
+											 "Chunk %u class %u block %u: Block overlaps or should have been merged "
+											 "with block: class %u block %u",
+											 chunkIdx, c, i, c2, j);
+						}
+						else
+						{
+							ANKI_SLAB_ASSERT(false,
+											 "Chunk %u class %u block %u: Block shouldn't be having the same address "
+											 "with: class %u block %u",
+											 chunkIdx, c, i, c2, j);
+						}
+					}
+				}
+			}
+		}
+
+		++chunkIdx;
+	}
+
+#undef ANKI_SLAB_ASSERT
+	return Error::NONE;
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+void SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::printFreeBlocks(StringListAuto& strList) const
+{
+	LockGuard<TLock> lock(m_lock);
+
+	U32 chunkCount = 0;
+	for(const TChunk* chunk : m_chunks)
+	{
+		strList.pushBackSprintf("Chunk #%u, total size %zu, free size %zu\n", chunkCount, chunk->m_totalSize,
+								chunk->m_freeSize);
+
+		for(U32 c = 0; c < TInterface::getClassCount(); ++c)
+		{
+			if(chunk->m_freeLists[c].getSize())
+			{
+				strList.pushBackSprintf("  Class #%u\n    ", c);
+			}
+
+			for(U32 i = 0; i < chunk->m_freeLists[c].getSize(); ++i)
+			{
+				const FreeBlock& blk = chunk->m_freeLists[c][i];
+				strList.pushBackSprintf("| %zu-%zu(%zu) ", blk.m_address, blk.m_address + blk.m_size - 1, blk.m_size);
+			}
+
+			if(chunk->m_freeLists[c].getSize())
+			{
+				strList.pushBack("|\n");
+			}
+		}
+
+		++chunkCount;
+	}
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+F32 SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::computeExternalFragmentation(PtrSize baseSize) const
+{
+	ANKI_ASSERT(baseSize > 0);
+
+	LockGuard<TLock> lock(m_lock);
+
+	F32 maxFragmentation = 0.0f;
+
+	for(const TChunk* chunk : m_chunks)
+	{
+		PtrSize largestFreeBlockSize = 0;
+
+		for(U32 c = 0; c < TInterface::getClassCount(); ++c)
+		{
+			for(const FreeBlock& block : chunk->m_freeLists[c])
+			{
+				largestFreeBlockSize = max(largestFreeBlockSize, block.m_size / baseSize);
+			}
+		}
+
+		const F32 frag = F32(1.0 - F64(largestFreeBlockSize) / F64(chunk->m_freeSize / baseSize));
+
+		maxFragmentation = max(maxFragmentation, frag);
+	}
+
+	return maxFragmentation;
+}
+
+template<typename TChunk, typename TInterface, typename TLock>
+F32 SegregatedListsAllocatorBuilder<TChunk, TInterface, TLock>::computeExternalFragmentationSawicki(
+	PtrSize baseSize) const
+{
+	ANKI_ASSERT(baseSize > 0);
+
+	LockGuard<TLock> lock(m_lock);
+
+	F32 maxFragmentation = 0.0f;
+
+	for(const TChunk* chunk : m_chunks)
+	{
+		F64 quality = 0.0;
+
+		for(U32 c = 0; c < TInterface::getClassCount(); ++c)
+		{
+			for(const FreeBlock& block : chunk->m_freeLists[c])
+			{
+				const F64 size = F64(block.m_size / baseSize);
+				quality += size * size;
+			}
+		}
+
+		quality = sqrt(quality) / F64(chunk->m_freeSize / baseSize);
+		const F32 frag = 1.0f - F32(quality * quality);
+
+		maxFragmentation = max(maxFragmentation, frag);
+	}
+
+	return maxFragmentation;
+}
+
+} // end namespace anki

+ 21 - 0
AnKi/Util/Thread.h

@@ -202,6 +202,27 @@ private:
 #endif
 };
 
+/// Dummy mutex. Used mainly in tests.
+class DummyMutex
+{
+public:
+	// Does nothing.
+	void lock()
+	{
+	}
+
+	// Does nothing.
+	void unlock()
+	{
+	}
+
+	// Does nothing.
+	Bool tryLock()
+	{
+		return true;
+	}
+};
+
 /// Read write mutex.
 class RWMutex
 {

+ 7 - 7
Tests/Framework/Framework.h

@@ -106,7 +106,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " != " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -119,7 +119,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " == " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -132,7 +132,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " > " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -145,7 +145,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " >= " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -158,7 +158,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " < " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -171,7 +171,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " <= " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 
@@ -186,7 +186,7 @@ extern void deleteTesterSingleton();
 			std::stringstream ss; \
 			ss << "FAILURE: " << #x << " != " << #y << " (" << file_ << ":" << line_ << ")"; \
 			fprintf(stderr, "%s\n", ss.str().c_str()); \
-			abort(); \
+			ANKI_DEBUG_BREAK(); \
 		} \
 	} while(0);
 

+ 203 - 0
Tests/Util/SegregatedListsAllocatorBuilder.cpp

@@ -0,0 +1,203 @@
+// Copyright (C) 2009-2022, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+#include <AnKi/Util/SegregatedListsAllocatorBuilder.h>
+#include <AnKi/Util/HighRezTimer.h>
+#include <Tests/Framework/Framework.h>
+
+using namespace anki;
+
+static constexpr U32 CLASS_COUNT = 6;
+
+class SegregatedListsAllocatorBuilderChunk :
+	public SegregatedListsAllocatorBuilderChunkBase<SegregatedListsAllocatorBuilderChunk, CLASS_COUNT>
+{
+};
+
+class SegregatedListsAllocatorBuilderInterface
+{
+public:
+	HeapAllocator<U8> m_alloc = {allocAligned, nullptr};
+	static constexpr PtrSize m_chunkSize = 100_MB;
+
+	static constexpr U32 getClassCount()
+	{
+		return CLASS_COUNT;
+	}
+
+	void getClassInfo(U32 idx, PtrSize& size) const
+	{
+		static const Array<PtrSize, getClassCount()> classes = {512_KB, 1_MB, 5_MB, 10_MB, 30_MB, m_chunkSize};
+		size = classes[idx];
+	}
+
+	Error allocateChunk(SegregatedListsAllocatorBuilderChunk*& newChunk, PtrSize& chunkSize)
+	{
+		newChunk = m_alloc.newInstance<SegregatedListsAllocatorBuilderChunk>();
+		chunkSize = m_chunkSize;
+		return Error::NONE;
+	}
+
+	void deleteChunk(SegregatedListsAllocatorBuilderChunk* chunk)
+	{
+		m_alloc.deleteInstance(chunk);
+	}
+
+	static constexpr PtrSize getMinSizeAlignment()
+	{
+		return 4;
+	}
+
+	HeapAllocator<U8>& getAllocator()
+	{
+		return m_alloc;
+	}
+
+	HeapAllocator<U8> getAllocator() const
+	{
+		return m_alloc;
+	}
+};
+
+using SLAlloc = SegregatedListsAllocatorBuilder<SegregatedListsAllocatorBuilderChunk,
+												SegregatedListsAllocatorBuilderInterface, DummyMutex>;
+
+template<typename TAlloc>
+static void printAllocatorBuilder(const TAlloc& sl)
+{
+	HeapAllocator<U8> alloc(allocAligned, nullptr);
+
+	StringListAuto list(alloc);
+	sl.printFreeBlocks(list);
+
+	if(list.isEmpty())
+	{
+		return;
+	}
+
+	StringAuto str(alloc);
+	list.join("", str);
+	printf("%s\n", str.cstr());
+}
+
+template<Bool T_VALIDATE, U32 T_ITERATIONS, Bool T_STATS>
+static void fuzzyTest()
+{
+	class Alloc
+	{
+	public:
+		SegregatedListsAllocatorBuilderChunk* m_chunk;
+		PtrSize m_address;
+		PtrSize m_alignment;
+		PtrSize m_size;
+	};
+
+	SLAlloc sl;
+	std::vector<Alloc> allocs;
+	allocs.reserve(T_ITERATIONS);
+
+	const Second start = HighRezTimer::getCurrentTime();
+	F64 avgFragmentation = 0.0f;
+	F64 maxFragmetation = 0.0f;
+
+	for(U32 i = 0; i < T_ITERATIONS; ++i)
+	{
+		const Bool doAllocation = (getRandom() % 2) == 0;
+		if(doAllocation)
+		{
+			Alloc alloc;
+			do
+			{
+				alloc.m_size = getRandom() % 70_MB;
+				alloc.m_alignment = nextPowerOfTwo(getRandom() % 16);
+			} while(alloc.m_size == 0 || alloc.m_alignment == 0);
+
+			ANKI_TEST_EXPECT_NO_ERR(sl.allocate(alloc.m_size, alloc.m_alignment, alloc.m_chunk, alloc.m_address));
+
+			allocs.push_back(alloc);
+		}
+		else if(allocs.size())
+		{
+			const U32 idx = U32(getRandom() % allocs.size());
+
+			const Alloc alloc = allocs[idx];
+			allocs.erase(allocs.begin() + idx);
+
+			sl.free(alloc.m_chunk, alloc.m_address, alloc.m_size);
+		}
+
+		if(T_STATS)
+		{
+			const F64 f = sl.computeExternalFragmentation();
+			avgFragmentation += f / F64(T_ITERATIONS);
+			maxFragmetation = max(maxFragmetation, f);
+		}
+
+		// printAllocatorBuilder(sl);
+		if(T_VALIDATE)
+		{
+			ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+		}
+	}
+
+	// Free the rest of the mem
+	for(const Alloc& alloc : allocs)
+	{
+		sl.free(alloc.m_chunk, alloc.m_address, alloc.m_size);
+	}
+
+	if(T_STATS)
+	{
+		const Second end = HighRezTimer::getCurrentTime();
+		const Second dt = end - start;
+		ANKI_TEST_LOGI("Operations/sec %f. Avg external fragmentation %f. Max external fragmentation %f",
+					   F64(T_ITERATIONS) / dt, avgFragmentation, maxFragmetation);
+	}
+}
+
+ANKI_TEST(Util, SegregatedListsAllocatorBuilder)
+{
+	// Simple test
+	{
+		SLAlloc sl;
+
+		SegregatedListsAllocatorBuilderChunk* chunk;
+		PtrSize address;
+		ANKI_TEST_EXPECT_NO_ERR(sl.allocate(66, 4, chunk, address));
+		// printAllocatorBuilder(sl);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+
+		SegregatedListsAllocatorBuilderChunk* chunk2;
+		PtrSize address2;
+		ANKI_TEST_EXPECT_NO_ERR(sl.allocate(512, 64, chunk2, address2));
+		// printAllocatorBuilder(sl);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+
+		SegregatedListsAllocatorBuilderChunk* chunk3;
+		PtrSize address3;
+		ANKI_TEST_EXPECT_NO_ERR(sl.allocate(4, 64, chunk3, address3));
+		// printAllocatorBuilder(sl);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+
+		sl.free(chunk, address2, 512);
+		// printAllocatorBuilder(sl);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+
+		sl.free(chunk, address3, 4);
+		// printAllocatorBuilder(sl);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+
+		sl.free(chunk, address, 66);
+		ANKI_TEST_EXPECT_NO_ERR(sl.validate());
+	}
+
+	// Fuzzy test
+	fuzzyTest<true, 1024, false>();
+}
+
+ANKI_TEST(Util, SegregatedListsAllocatorBuilderBenchmark)
+{
+	fuzzyTest<false, 2000000, true>();
+}