diff --git a/Code/Framework/AzCore/AzCore/Memory/AllocatorBase.cpp b/Code/Framework/AzCore/AzCore/Memory/AllocatorBase.cpp index e450f12bcf..40ddff0fc3 100644 --- a/Code/Framework/AzCore/AzCore/Memory/AllocatorBase.cpp +++ b/Code/Framework/AzCore/AzCore/Memory/AllocatorBase.cpp @@ -12,6 +12,49 @@ using namespace AZ; +#define RECORDING_ENABLED 0 + +#if RECORDING_ENABLED + +struct AllocatorOperation +{ + enum OperationType : unsigned int + { + ALLOCATE, + DEALLOCATE, + REALLOCATE, + RESIZE + }; + OperationType m_operationType : 2; + size_t m_size : 46; + size_t m_alignment : 16; + void* m_ptr; + void* m_newptr; // required for resize +}; +static constexpr size_t s_allocationOperationCount = 5 * 1024; +static AZStd::array s_operations = {}; +static uint64_t s_operationCounter = 0; +static AZStd::mutex s_operationsMutex; + +AllocatorOperation& GetNextAllocatorOperation() +{ + AZStd::scoped_lock lock(s_operationsMutex); + if (s_operationCounter == s_allocationOperationCount) + { + FILE* file = nullptr; + fopen_s(&file, "memoryrecordings.bin", "ab"); + if (file) + { + fwrite(&s_operations, sizeof(AllocatorOperation), s_allocationOperationCount, file); + fclose(file); + } + s_operationCounter = 0; + } + return s_operations[s_operationCounter++]; +} + +#endif + AllocatorBase::AllocatorBase(IAllocatorAllocate* allocationSource, const char* name, const char* desc) : IAllocator(allocationSource), m_name(name), @@ -136,6 +179,16 @@ void AllocatorBase::ProfileAllocation(void* ptr, size_t byteSize, size_t alignme EBUS_EVENT(AZ::Debug::MemoryDrillerBus, RegisterAllocation, this, ptr, byteSize, alignment, name, fileName, lineNum, suppressStackRecord); #endif } + +#if RECORDING_ENABLED + { + AllocatorOperation& op = GetNextAllocatorOperation(); + op.m_operationType = AllocatorOperation::ALLOCATE; + op.m_size = byteSize; + op.m_alignment = alignment; + op.m_ptr = ptr; + } +#endif } void AllocatorBase::ProfileDeallocation(void* ptr, size_t byteSize, size_t alignment, Debug::AllocationInfo* info) @@ -148,6 +201,15 @@ void AllocatorBase::ProfileDeallocation(void* ptr, size_t byteSize, size_t align EBUS_EVENT(AZ::Debug::MemoryDrillerBus, UnregisterAllocation, this, ptr, byteSize, alignment, info); #endif } +#if RECORDING_ENABLED + { + AllocatorOperation& op = GetNextAllocatorOperation(); + op.m_operationType = AllocatorOperation::DEALLOCATE; + op.m_size = byteSize; + op.m_alignment = alignment; + op.m_ptr = ptr; + } +#endif } void AllocatorBase::ProfileReallocationBegin(void* ptr, size_t newSize) @@ -174,6 +236,16 @@ void AllocatorBase::ProfileReallocationEnd(void* ptr, void* newPtr, size_t newSi EBUS_EVENT(AZ::Debug::MemoryDrillerBus, ReallocateAllocation, this, ptr, newPtr, newSize, newAlignment); #endif } +#if RECORDING_ENABLED + { + AllocatorOperation& op = GetNextAllocatorOperation(); + op.m_operationType = AllocatorOperation::REALLOCATE; + op.m_size = newSize; + op.m_alignment = newAlignment; + op.m_ptr = ptr; + op.m_newptr = newPtr; + } +#endif } void AllocatorBase::ProfileReallocation(void* ptr, void* newPtr, size_t newSize, size_t newAlignment) @@ -187,6 +259,15 @@ void AllocatorBase::ProfileResize(void* ptr, size_t newSize) { EBUS_EVENT(AZ::Debug::MemoryDrillerBus, ResizeAllocation, this, ptr, newSize); } +#if RECORDING_ENABLED + { + AllocatorOperation& op = GetNextAllocatorOperation(); + op.m_operationType = AllocatorOperation::RESIZE; + op.m_size = newSize; + op.m_alignment = 0; + op.m_ptr = ptr; + } +#endif } bool AllocatorBase::OnOutOfMemory(size_t byteSize, size_t alignment, int flags, const char* name, const char* fileName, int lineNum) diff --git a/Code/Framework/AzCore/Tests/Memory/AllocatorBenchmarks.cpp b/Code/Framework/AzCore/Tests/Memory/AllocatorBenchmarks.cpp index dfaefd4c0f..76c3316009 100644 --- a/Code/Framework/AzCore/Tests/Memory/AllocatorBenchmarks.cpp +++ b/Code/Framework/AzCore/Tests/Memory/AllocatorBenchmarks.cpp @@ -32,7 +32,7 @@ namespace Benchmark size_t GetMemorySize(void* memory); } - static AZ::Debug::DrillerManager* s_drillerManager = nullptr; + //static AZ::Debug::DrillerManager* s_drillerManager = nullptr; /// /// Test allocator wrapper that redirects the calls to the passed TAllocator by using AZ::AllocatorInstance. @@ -47,14 +47,14 @@ namespace Benchmark { AZ::AllocatorInstance::Create(); - s_drillerManager = AZ::Debug::DrillerManager::Create(); - s_drillerManager->Register(aznew AZ::Debug::MemoryDriller); + /*s_drillerManager = AZ::Debug::DrillerManager::Create(); + s_drillerManager->Register(aznew AZ::Debug::MemoryDriller);*/ } static void TearDown() { - AZ::Debug::DrillerManager::Destroy(s_drillerManager); - s_drillerManager = nullptr; + /*AZ::Debug::DrillerManager::Destroy(s_drillerManager); + s_drillerManager = nullptr;*/ AZ::AllocatorInstance::Destroy(); } @@ -89,51 +89,41 @@ namespace Benchmark return AZ::AllocatorInstance::Get().NumAllocatedBytes() + AZ::AllocatorInstance::Get().GetUnAllocatedMemory(); } + + static size_t GetSize(void* ptr) + { + return AZ::AllocatorInstance::Get().AllocationSize(ptr); + } }; /// /// Basic allocator used as a baseline. This allocator is the most basic allocation possible with the OS (AZ_OS_MALLOC). /// MallocSchema cannot be used here because it has extra logic that we don't want to use as a baseline. /// - class TestRawMallocAllocator - : public AZ::AllocatorBase - , public AZ::IAllocatorAllocate + class TestRawMallocAllocator {}; + + template<> + class TestAllocatorWrapper { public: - AZ_TYPE_INFO(TestMallocSchemaAllocator, "{08EB400A-D723-46C6-808E-D0844C8DE206}"); - - struct Descriptor {}; - - TestRawMallocAllocator() - : AllocatorBase(this, "TestRawMallocAllocator", "") + TestAllocatorWrapper() { - m_numAllocatedBytes = 0; + s_numAllocatedBytes = 0; } - bool Create(const Descriptor&) + static void SetUp() { - m_numAllocatedBytes = 0; - return true; + s_numAllocatedBytes = 0; } - // IAllocator - void Destroy() override + static void TearDown() { - m_numAllocatedBytes = 0; - } - AZ::AllocatorDebugConfig GetDebugConfig() override - { - return AZ::AllocatorDebugConfig(); - } - AZ::IAllocatorAllocate* GetSchema() override - { - return nullptr; } // IAllocatorAllocate - void* Allocate(size_t byteSize, size_t alignment, int = 0, const char* = 0, const char* = 0, int = 0, unsigned int = 0) override + static void* Allocate(size_t byteSize, size_t alignment) { - m_numAllocatedBytes += byteSize; + s_numAllocatedBytes += byteSize; if (alignment) { return AZ_OS_MALLOC(byteSize, alignment); @@ -144,18 +134,18 @@ namespace Benchmark } } - void DeAllocate(void* ptr, size_t = 0, size_type = 0) override + static void DeAllocate(void* ptr, size_t = 0) { - m_numAllocatedBytes -= Platform::GetMemorySize(ptr); + s_numAllocatedBytes -= Platform::GetMemorySize(ptr); AZ_OS_FREE(ptr); } - void* ReAllocate(void* ptr, size_t newSize, size_t newAlignment) override + static void* ReAllocate(void* ptr, size_t newSize, size_t newAlignment) { - m_numAllocatedBytes -= Platform::GetMemorySize(ptr); + s_numAllocatedBytes -= Platform::GetMemorySize(ptr); AZ_OS_FREE(ptr); - m_numAllocatedBytes += newSize; + s_numAllocatedBytes += newSize; if (newAlignment) { return AZ_OS_MALLOC(newSize, newAlignment); @@ -166,7 +156,7 @@ namespace Benchmark } } - size_t Resize(void* ptr, size_t newSize) override + static size_t Resize(void* ptr, size_t newSize) { AZ_UNUSED(ptr); AZ_UNUSED(newSize); @@ -174,45 +164,24 @@ namespace Benchmark return 0; } - size_t AllocationSize(void* ptr) override + static void GarbageCollect() {} + + static size_t NumAllocatedBytes() + { + return s_numAllocatedBytes; + } + + static size_t GetSize(void* ptr) { return Platform::GetMemorySize(ptr); } - void GarbageCollect() override {} - - size_t NumAllocatedBytes() const override - { - return m_numAllocatedBytes; - } - - size_t Capacity() const override - { - return AZ_CORE_MAX_ALLOCATOR_SIZE; // unused - } - - size_t GetMaxAllocationSize() const override - { - return AZ_CORE_MAX_ALLOCATOR_SIZE; // unused - } - - size_t GetMaxContiguousAllocationSize() const override - { - return AZ_CORE_MAX_ALLOCATOR_SIZE; // unused - } - size_t GetUnAllocatedMemory(bool = false) const override - { - return 0; // unused - } - IAllocatorAllocate* GetSubAllocator() override - { - return nullptr; // unused - } - private: - size_t m_numAllocatedBytes; + static size_t s_numAllocatedBytes; }; + size_t TestAllocatorWrapper::s_numAllocatedBytes = 0; + // Here we require to implement this to be able to configure a name for the allocator, otherswise the AllocatorManager crashes when trying to configure the overrides class TestMallocSchemaAllocator : public AZ::SimpleSchemaAllocator { @@ -250,11 +219,14 @@ namespace Benchmark AZ_TYPE_INFO(TestSystemAllocator, "{360D4DAA-D65D-4D5C-A6FA-1A4C5261C35C}"); }; - // Allocated bytes reported by the allocator / actually requested bytes - static const char* s_counterAllocatorMemoryRatio = "Allocator_MemoryRatio"; + // Allocated bytes reported by the allocator + static const char* s_counterAllocatorMemory = "Allocator_Memory"; - // Allocated bytes reported by the process / actually requested bytes - static const char* s_counterProcessMemoryRatio = "Process_MemoryRatio"; + // Allocated bytes reported by the process + static const char* s_counterProcessMemory = "Process_Memory"; + + // Allocated bytes as counted by the benchmark + static const char* s_counterBenchmarkMemory = "Benchmark_Memory"; enum AllocationSize { @@ -340,16 +312,6 @@ namespace Benchmark using base = AllocatorBenchmarkFixture; using TestAllocatorType = typename base::TestAllocatorType; - void internalSetUp(const ::benchmark::State& state) override - { - AllocatorBenchmarkFixture::internalSetUp(state); - } - - void internalTearDown(const ::benchmark::State& state) override - { - AllocatorBenchmarkFixture::internalTearDown(state); - } - public: void Benchmark(benchmark::State& state) { @@ -372,16 +334,9 @@ namespace Benchmark state.PauseTiming(); } - // In allocation cases, s_counterAllocatorMemoryRatio is measuring how much over-allocation our allocators - // are doing to keep track of the memory and because of fragmentation/under-use of blocks. A ratio over 1 means - // that we are using more memory than requested. Ideally we would approximate to a ratio of 1. - state.counters[s_counterAllocatorMemoryRatio] = benchmark::Counter( - static_cast(TestAllocatorType::NumAllocatedBytes()) / static_cast(totalAllocationSize), - benchmark::Counter::kDefaults); - // s_counterProcessMemoryRatio is measuring the same ratio but using the OS to measure the used process memory - state.counters[s_counterProcessMemoryRatio] = benchmark::Counter( - static_cast(Platform::GetProcessMemoryUsageBytes() - processMemoryBaseline) / static_cast(totalAllocationSize), - benchmark::Counter::kDefaults); + state.counters[s_counterAllocatorMemory] = benchmark::Counter(static_cast(TestAllocatorType::NumAllocatedBytes()), benchmark::Counter::kDefaults); + state.counters[s_counterProcessMemory] = benchmark::Counter(static_cast(Platform::GetProcessMemoryUsageBytes() - processMemoryBaseline), benchmark::Counter::kDefaults); + state.counters[s_counterBenchmarkMemory] = benchmark::Counter(static_cast(totalAllocationSize), benchmark::Counter::kDefaults); for (size_t allocationIndex = 0; allocationIndex < numberOfAllocations; ++allocationIndex) { @@ -404,15 +359,6 @@ namespace Benchmark using base = AllocatorBenchmarkFixture; using TestAllocatorType = typename base::TestAllocatorType; - void internalSetUp(const ::benchmark::State& state) override - { - AllocatorBenchmarkFixture::internalSetUp(state); - } - - void internalTearDown(const ::benchmark::State& state) override - { - AllocatorBenchmarkFixture::internalTearDown(state); - } public: void Benchmark(benchmark::State& state) { @@ -442,17 +388,9 @@ namespace Benchmark perThreadAllocations[allocationIndex] = nullptr; } - // In deallocation cases, s_counterAllocatorMemoryRatio is measuring how much "left-over" memory our allocators - // have after deallocations happen. This is memory that is not returned to the operative system. A ratio of 1 means - // that no memory was returned to the OS. A ratio over 1 means that we are holding more memory than requested. A ratio - // lower than 1 means that we have returned some memory. - state.counters[s_counterAllocatorMemoryRatio] = benchmark::Counter( - static_cast(TestAllocatorType::NumAllocatedBytes()) / static_cast(totalAllocationSize), - benchmark::Counter::kDefaults); - // s_counterProcessMemoryRatio is measuring the same ratio but using the OS to measure the used process memory - state.counters[s_counterProcessMemoryRatio] = benchmark::Counter( - static_cast(Platform::GetProcessMemoryUsageBytes() - processMemoryBaseline) / static_cast(totalAllocationSize), - benchmark::Counter::kDefaults); + state.counters[s_counterAllocatorMemory] = benchmark::Counter(static_cast(TestAllocatorType::NumAllocatedBytes()), benchmark::Counter::kDefaults); + state.counters[s_counterProcessMemory] = benchmark::Counter(static_cast(Platform::GetProcessMemoryUsageBytes() - processMemoryBaseline), benchmark::Counter::kDefaults); + state.counters[s_counterBenchmarkMemory] = benchmark::Counter(static_cast(totalAllocationSize), benchmark::Counter::kDefaults); state.SetItemsProcessed(numberOfAllocations); @@ -460,6 +398,175 @@ namespace Benchmark } } }; + + template + class RecordedAllocationBenchmarkFixture : public AllocatorBenchmarkFixture + { + using base = AllocatorBenchmarkFixture; + using TestAllocatorType = typename base::TestAllocatorType; + + struct AllocatorOperation + { + enum OperationType : unsigned int + { + ALLOCATE, + DEALLOCATE, + REALLOCATE, + RESIZE + }; + OperationType m_operationType : 2; + size_t m_size : 46; + size_t m_alignment : 16; + void* m_ptr; + void* m_newptr; // required for resize + }; + + public: + void Benchmark(benchmark::State& state) + { + for (auto _ : state) + { + state.PauseTiming(); + + AZStd::unordered_map pointerRemapping; + AZStd::unordered_map allocationSize; + constexpr size_t allocationOperationCount = 5 * 1024; + AZStd::array m_operations = {}; + + FILE* file = nullptr; + fopen_s(&file, "memoryrecordings.bin", "rb"); + if (!file) + { + return; + } + size_t elementsRead = fread(&m_operations, sizeof(AllocatorOperation), allocationOperationCount, file); + size_t totalElementsRead = elementsRead; + const size_t processMemoryBaseline = Platform::GetProcessMemoryUsageBytes(); + size_t totalAllocationSize = 0; + + while (elementsRead > 0) + { + for (size_t operationIndex = 0; operationIndex < elementsRead; ++operationIndex) + { + const AllocatorOperation& operation = m_operations[operationIndex]; + switch (operation.m_operationType) + { + case AllocatorOperation::ALLOCATE: + { + if (operation.m_ptr) + { + const auto it = pointerRemapping.emplace(operation.m_ptr, nullptr); + if (it.second) // otherwise already allocated + { + state.ResumeTiming(); + void* ptr = TestAllocatorType::Allocate(operation.m_size, operation.m_alignment); + state.PauseTiming(); + totalAllocationSize += operation.m_size; + it.first->second = ptr; + allocationSize[ptr] = operation.m_size; + } + else + { + //AZ_Warning("RecordedAllocationBenchmarkFixture", false, "Allocation on %p was already made", operation.m_ptr); + } + } + break; + } + case AllocatorOperation::DEALLOCATE: + { + if (operation.m_ptr) // some deallocate(nullptr) are recorded + { + const auto ptrIt = pointerRemapping.find(operation.m_ptr); + if (ptrIt != pointerRemapping.end()) + { + totalAllocationSize -= allocationSize[ptrIt->second]; + state.ResumeTiming(); + TestAllocatorType::DeAllocate(ptrIt->second, /*operation.m_size*/ 0); // size is not correct after a resize, a 0 size deals with it + state.PauseTiming(); + pointerRemapping.erase(ptrIt); + } + } + else + { + // Just to account of the call of deallocate(nullptr); + // totalAllocationSize -= 0; // No real deallocation happened + state.ResumeTiming(); + TestAllocatorType::DeAllocate(operation.m_ptr, /*operation.m_size*/ 0); + state.PauseTiming(); + } + break; + } + case AllocatorOperation::REALLOCATE: + { + void* ptr = nullptr; + if (operation.m_ptr) + { + AZ_Assert(operation.m_newptr, "Need to consider other cases?"); + const auto ptrIt = pointerRemapping.find(operation.m_ptr); + AZ_Assert(ptrIt != pointerRemapping.end(), "Missing allocation for reallocation"); // In case the recording didnt catch something + ptr = ptrIt->second; + pointerRemapping.erase(ptrIt); + } + AZ_Assert(operation.m_newptr != nullptr, "Reallocation failed in the game"); + const auto it = pointerRemapping.emplace(operation.m_newptr, nullptr); + if (it.second) + { + totalAllocationSize -= allocationSize[ptr]; + state.ResumeTiming(); + void* newPtr = TestAllocatorType::ReAllocate(ptr, operation.m_size, operation.m_alignment); + state.PauseTiming(); + totalAllocationSize += operation.m_size; + it.first->second = newPtr; + allocationSize[newPtr] = operation.m_size; + } + else + { + totalAllocationSize -= allocationSize[ptr]; + state.ResumeTiming(); + TestAllocatorType::DeAllocate(ptr); + state.PauseTiming(); + } + break; + } + case AllocatorOperation::RESIZE: + { + const auto ptrIt = pointerRemapping.find(operation.m_ptr); + AZ_Assert(ptrIt != pointerRemapping.end(), "Missing allocation for resize"); // In case the recording didnt catch something + totalAllocationSize -= allocationSize[ptrIt->second]; + state.ResumeTiming(); + TestAllocatorType::Resize(ptrIt->second, operation.m_size); + state.PauseTiming(); + totalAllocationSize += operation.m_size; + if (operation.m_size == 0) + { + pointerRemapping.erase(ptrIt); + } + break; + } + } + } + + elementsRead = fread(&m_operations, sizeof(AllocatorOperation), allocationOperationCount, file); + totalElementsRead += elementsRead; + } + fclose(file); + + state.counters[s_counterAllocatorMemory] = benchmark::Counter(static_cast(TestAllocatorType::NumAllocatedBytes()), benchmark::Counter::kDefaults); + state.counters[s_counterProcessMemory] = benchmark::Counter(static_cast(Platform::GetProcessMemoryUsageBytes() - processMemoryBaseline), benchmark::Counter::kDefaults); + state.counters[s_counterBenchmarkMemory] = benchmark::Counter(static_cast(totalAllocationSize), benchmark::Counter::kDefaults); + + state.SetItemsProcessed(totalElementsRead); + + // Deallocate the remainder (since we stopped the recording middle-game)(there are leaks as well) + for (const auto& pointerMapping : pointerRemapping) + { + TestAllocatorType::DeAllocate(pointerMapping.second); + } + pointerRemapping.clear(); + TestAllocatorType::GarbageCollect(); + } + } + }; // For non-threaded ranges, run 100, 400, 1600 amounts static void RunRanges(benchmark::internal::Benchmark* b) @@ -469,6 +576,10 @@ namespace Benchmark b->Arg((1 << i) * 100); } } + static void RecordedRunRanges(benchmark::internal::Benchmark* b) + { + b->Arg(1); + } // For threaded ranges, run just 200, multi-threaded will already multiply by thread static void ThreadedRunRanges(benchmark::internal::Benchmark* b) @@ -494,16 +605,17 @@ namespace Benchmark #define BM_REGISTER_ALLOCATOR(TESTNAME, ALLOCATORTYPE) \ namespace TESTNAME \ { \ - BM_REGISTER_SIZE_FIXTURES(AllocationBenchmarkFixture, TESTNAME, ALLOCATORTYPE) \ - BM_REGISTER_SIZE_FIXTURES(DeAllocationBenchmarkFixture, TESTNAME, ALLOCATORTYPE) \ + BM_REGISTER_SIZE_FIXTURES(AllocationBenchmarkFixture, TESTNAME, ALLOCATORTYPE); \ + BM_REGISTER_SIZE_FIXTURES(DeAllocationBenchmarkFixture, TESTNAME, ALLOCATORTYPE); \ + BM_REGISTER_TEMPLATE(RecordedAllocationBenchmarkFixture, TESTNAME, ALLOCATORTYPE)->Apply(RecordedRunRanges); \ } BM_REGISTER_ALLOCATOR(RawMallocAllocator, TestRawMallocAllocator); BM_REGISTER_ALLOCATOR(MallocSchemaAllocator, TestMallocSchemaAllocator); - BM_REGISTER_ALLOCATOR(HeapSchemaAllocator, TestHeapSchemaAllocator); BM_REGISTER_ALLOCATOR(HphaSchemaAllocator, TestHphaSchemaAllocator); BM_REGISTER_ALLOCATOR(SystemAllocator, TestSystemAllocator); + //BM_REGISTER_ALLOCATOR(HeapSchemaAllocator, TestHeapSchemaAllocator); // Requires to pre-allocate blocks and cannot work as a general-purpose allocator //BM_REGISTER_SCHEMA(BestFitExternalMapSchema); // Requires to implement AZ::IAllocatorAllocate //BM_REGISTER_SCHEMA(PoolSchema); // Requires special alignment requests while allocating