diff options
| author | Stefan Boberg <[email protected]> | 2024-11-25 09:56:23 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-11-25 09:56:23 +0100 |
| commit | 8b8de92e51db4cc4c1727712c736dcba5f79d369 (patch) | |
| tree | 1f58edaaad389837a7652daebab246125762240e /src/zencore/memory | |
| parent | 5.5.13 (diff) | |
| download | zen-8b8de92e51db4cc4c1727712c736dcba5f79d369.tar.xz zen-8b8de92e51db4cc4c1727712c736dcba5f79d369.zip | |
Insights-compatible memory tracking (#214)
This change introduces support for tracing of memory allocation activity. The code is ported from UE5, and Unreal Insights can be used to analyze the output. This is currently only fully supported on Windows, but will be extended to Mac/Linux in the near future.
To activate full memory tracking, pass `--trace=memory` on the commandline alongside `--tracehost=<ip>` or `-tracefile=<path>`. For more control over how much detail is traced you can instead pass some combination of `callstack`, `memtag`, `memalloc` instead. In practice, `--trace=memory` is an alias for `--trace=callstack,memtag,memalloc`). For convenience we also support `--trace=memory_light` which omits call stacks.
This change also introduces multiple memory allocators, which may be selected via command-line option `--malloc=<allocator>`:
* `mimalloc` - mimalloc (default, same as before)
* `rpmalloc` - rpmalloc is another high performance allocator for multithreaded applications which may be a better option than mimalloc (to be evaluated). Due to toolchain limitations this is currently only supported on Windows.
* `stomp` - an allocator intended to be used during development/debugging to help track down memory issues such as use-after-free or out-of-bounds access. Currently only supported on Windows.
* `ansi` - fallback to default system allocator
Diffstat (limited to 'src/zencore/memory')
| -rw-r--r-- | src/zencore/memory/fmalloc.cpp | 156 | ||||
| -rw-r--r-- | src/zencore/memory/mallocansi.cpp | 251 | ||||
| -rw-r--r-- | src/zencore/memory/mallocmimalloc.cpp | 197 | ||||
| -rw-r--r-- | src/zencore/memory/mallocrpmalloc.cpp | 189 | ||||
| -rw-r--r-- | src/zencore/memory/mallocstomp.cpp | 283 | ||||
| -rw-r--r-- | src/zencore/memory/memory.cpp | 281 |
6 files changed, 1357 insertions, 0 deletions
diff --git a/src/zencore/memory/fmalloc.cpp b/src/zencore/memory/fmalloc.cpp new file mode 100644 index 000000000..3e96003f5 --- /dev/null +++ b/src/zencore/memory/fmalloc.cpp @@ -0,0 +1,156 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <string.h> +#include <zencore/memory/fmalloc.h> +#include <zencore/memory/memory.h> + +namespace zen { + +////////////////////////////////////////////////////////////////////////// + +class FInitialMalloc : public FMalloc +{ + virtual void* Malloc(size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->Malloc(Count, Alignment); + } + virtual void* TryMalloc(size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->TryMalloc(Count, Alignment); + } + virtual void* Realloc(void* Original, size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->Realloc(Original, Count, Alignment); + } + virtual void* TryRealloc(void* Original, size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->TryRealloc(Original, Count, Alignment); + } + virtual void Free(void* Original) override + { + Memory::Initialize(); + return GMalloc->Free(Original); + } + virtual void* MallocZeroed(size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->MallocZeroed(Count, Alignment); + } + + virtual void* TryMallocZeroed(size_t Count, uint32_t Alignment = DEFAULT_ALIGNMENT) override + { + Memory::Initialize(); + return GMalloc->TryMallocZeroed(Count, Alignment); + } + virtual size_t QuantizeSize(size_t Count, uint32_t Alignment) override + { + Memory::Initialize(); + return GMalloc->QuantizeSize(Count, Alignment); + } + virtual bool GetAllocationSize(void* Original, size_t& SizeOut) override + { + Memory::Initialize(); + return GMalloc->GetAllocationSize(Original, SizeOut); + } + virtual void OnMallocInitialized() override {} + virtual void Trim(bool bTrimThreadCaches) override { ZEN_UNUSED(bTrimThreadCaches); } +} GInitialMalloc; + +FMalloc* GMalloc = &GInitialMalloc; /* Memory allocator */ + +////////////////////////////////////////////////////////////////////////// + +void* +FUseSystemMallocForNew::operator new(size_t Size) +{ + return Memory::SystemMalloc(Size); +} + +void +FUseSystemMallocForNew::operator delete(void* Ptr) +{ + Memory::SystemFree(Ptr); +} + +void* +FUseSystemMallocForNew::operator new[](size_t Size) +{ + return Memory::SystemMalloc(Size); +} + +void +FUseSystemMallocForNew::operator delete[](void* Ptr) +{ + Memory::SystemFree(Ptr); +} + +////////////////////////////////////////////////////////////////////////// + +void* +FMalloc::TryRealloc(void* Original, size_t Count, uint32_t Alignment) +{ + return Realloc(Original, Count, Alignment); +} + +void* +FMalloc::TryMalloc(size_t Count, uint32_t Alignment) +{ + return Malloc(Count, Alignment); +} + +void* +FMalloc::TryMallocZeroed(size_t Count, uint32_t Alignment) +{ + return MallocZeroed(Count, Alignment); +} + +void* +FMalloc::MallocZeroed(size_t Count, uint32_t Alignment) +{ + void* const Memory = Malloc(Count, Alignment); + + if (Memory) + { + ::memset(Memory, 0, Count); + } + + return Memory; +} + +void +FMalloc::OutOfMemory(size_t Size, uint32_t Alignment) +{ + ZEN_UNUSED(Size, Alignment); + // no-op by default +} + +void +FMalloc::Trim(bool bTrimThreadCaches) +{ + ZEN_UNUSED(bTrimThreadCaches); +} + +void +FMalloc::OnMallocInitialized() +{ +} + +bool +FMalloc::GetAllocationSize(void* Original, size_t& SizeOut) +{ + ZEN_UNUSED(Original, SizeOut); + return false; // Generic implementation has no way of determining this +} + +size_t +FMalloc::QuantizeSize(size_t Count, uint32_t Alignment) +{ + ZEN_UNUSED(Alignment); + return Count; // Generic implementation has no way of determining this +} + +} // namespace zen diff --git a/src/zencore/memory/mallocansi.cpp b/src/zencore/memory/mallocansi.cpp new file mode 100644 index 000000000..9c3936172 --- /dev/null +++ b/src/zencore/memory/mallocansi.cpp @@ -0,0 +1,251 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/memory/mallocansi.h> + +#include <zencore/intmath.h> +#include <zencore/memory/align.h> +#include <zencore/windows.h> + +#if ZEN_PLATFORM_LINUX +# define PLATFORM_USE_ANSI_POSIX_MALLOC 1 +#endif + +#if ZEN_PLATFORM_MAC +# define PLATFORM_USE_CUSTOM_MEMALIGN 1 +#endif + +#ifndef PLATFORM_USE_ANSI_MEMALIGN +# define PLATFORM_USE_ANSI_MEMALIGN 0 +#endif + +#ifndef PLATFORM_USE_ANSI_POSIX_MALLOC +# define PLATFORM_USE_ANSI_POSIX_MALLOC 0 +#endif + +#ifndef PLATFORM_USE_CUSTOM_MEMALIGN +# define PLATFORM_USE_CUSTOM_MEMALIGN 0 +#endif + +#if PLATFORM_USE_ANSI_POSIX_MALLOC +# include <malloc.h> +# include <string.h> +#endif + +#define MALLOC_ANSI_USES__ALIGNED_MALLOC ZEN_PLATFORM_WINDOWS + +namespace zen { + +////////////////////////////////////////////////////////////////////////// + +void* +AnsiMalloc(size_t Size, uint32_t Alignment) +{ +#if MALLOC_ANSI_USES__ALIGNED_MALLOC + void* Result = _aligned_malloc(Size, Alignment); +#elif PLATFORM_USE_ANSI_POSIX_MALLOC + void* Result; + if (posix_memalign(&Result, Alignment, Size) != 0) + { + Result = nullptr; + } +#elif PLATFORM_USE_ANSI_MEMALIGN + Result = reallocalign(Ptr, NewSize, Alignment); +#elif PLATFORM_USE_CUSTOM_MEMALIGN + void* Ptr = malloc(Size + Alignment + sizeof(void*) + sizeof(size_t)); + void* Result = nullptr; + if (Ptr) + { + Result = Align((uint8_t*)Ptr + sizeof(void*) + sizeof(size_t), Alignment); + *((void**)((uint8_t*)Result - sizeof(void*))) = Ptr; + *((size_t*)((uint8_t*)Result - sizeof(void*) - sizeof(size_t))) = Size; + } +#else +# error Unknown allocation path +#endif + + return Result; +} + +size_t +AnsiGetAllocationSize(void* Original) +{ +#if MALLOC_ANSI_USES__ALIGNED_MALLOC + return _aligned_msize(Original, 16, 0); // TODO: incorrectly assumes alignment of 16 +#elif PLATFORM_USE_ANSI_POSIX_MALLOC || PLATFORM_USE_ANSI_MEMALIGN + return malloc_usable_size(Original); +#elif PLATFORM_USE_CUSTOM_MEMALIGN + return *((size_t*)((uint8_t*)Original - sizeof(void*) - sizeof(size_t))); +#else +# error Unknown allocation path +#endif +} + +void* +AnsiRealloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ + void* Result = nullptr; + +#if MALLOC_ANSI_USES__ALIGNED_MALLOC + if (Ptr && NewSize) + { + Result = _aligned_realloc(Ptr, NewSize, Alignment); + } + else if (Ptr == nullptr) + { + Result = _aligned_malloc(NewSize, Alignment); + } + else + { + _aligned_free(Ptr); + Result = nullptr; + } +#elif PLATFORM_USE_ANSI_POSIX_MALLOC + if (Ptr && NewSize) + { + size_t UsableSize = malloc_usable_size(Ptr); + if (posix_memalign(&Result, Alignment, NewSize) != 0) + { + Result = nullptr; + } + else if (UsableSize) + { + memcpy(Result, Ptr, Min(NewSize, UsableSize)); + } + free(Ptr); + } + else if (Ptr == nullptr) + { + if (posix_memalign(&Result, Alignment, NewSize) != 0) + { + Result = nullptr; + } + } + else + { + free(Ptr); + Result = nullptr; + } +#elif PLATFORM_USE_CUSTOM_MEMALIGN + if (Ptr && NewSize) + { + // Can't use realloc as it might screw with alignment. + Result = AnsiMalloc(NewSize, Alignment); + size_t PtrSize = AnsiGetAllocationSize(Ptr); + memcpy(Result, Ptr, Min(NewSize, PtrSize)); + AnsiFree(Ptr); + } + else if (Ptr == nullptr) + { + Result = AnsiMalloc(NewSize, Alignment); + } + else + { + free(*((void**)((uint8_t*)Ptr - sizeof(void*)))); + Result = nullptr; + } +#else +# error Unknown allocation path +#endif + + return Result; +} + +void +AnsiFree(void* Ptr) +{ +#if MALLOC_ANSI_USES__ALIGNED_MALLOC + _aligned_free(Ptr); +#elif PLATFORM_USE_ANSI_POSIX_MALLOC || PLATFORM_USE_ANSI_MEMALIGN + free(Ptr); +#elif PLATFORM_USE_CUSTOM_MEMALIGN + if (Ptr) + { + free(*((void**)((uint8_t*)Ptr - sizeof(void*)))); + } +#else +# error Unknown allocation path +#endif +} + +////////////////////////////////////////////////////////////////////////// + +FMallocAnsi::FMallocAnsi() +{ +#if ZEN_PLATFORM_WINDOWS + // Enable low fragmentation heap - http://msdn2.microsoft.com/en-US/library/aa366750.aspx + intptr_t CrtHeapHandle = _get_heap_handle(); + ULONG EnableLFH = 2; + HeapSetInformation((void*)CrtHeapHandle, HeapCompatibilityInformation, &EnableLFH, sizeof(EnableLFH)); +#endif +} + +void* +FMallocAnsi::TryMalloc(size_t Size, uint32_t Alignment) +{ + Alignment = Max(Size >= 16 ? (uint32_t)16 : (uint32_t)8, Alignment); + + void* Result = AnsiMalloc(Size, Alignment); + + return Result; +} + +void* +FMallocAnsi::Malloc(size_t Size, uint32_t Alignment) +{ + void* Result = TryMalloc(Size, Alignment); + + if (Result == nullptr && Size) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} + +void* +FMallocAnsi::TryRealloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ + Alignment = Max(NewSize >= 16 ? (uint32_t)16 : (uint32_t)8, Alignment); + + void* Result = AnsiRealloc(Ptr, NewSize, Alignment); + + return Result; +} + +void* +FMallocAnsi::Realloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ + void* Result = TryRealloc(Ptr, NewSize, Alignment); + + if (Result == nullptr && NewSize != 0) + { + OutOfMemory(NewSize, Alignment); + } + + return Result; +} + +void +FMallocAnsi::Free(void* Ptr) +{ + AnsiFree(Ptr); +} + +bool +FMallocAnsi::GetAllocationSize(void* Original, size_t& SizeOut) +{ + if (!Original) + { + return false; + } + +#if MALLOC_ANSI_USES__ALIGNED_MALLOC + ZEN_UNUSED(SizeOut); + return false; +#else + SizeOut = AnsiGetAllocationSize(Original); + return true; +#endif +} + +} // namespace zen diff --git a/src/zencore/memory/mallocmimalloc.cpp b/src/zencore/memory/mallocmimalloc.cpp new file mode 100644 index 000000000..1919af3bf --- /dev/null +++ b/src/zencore/memory/mallocmimalloc.cpp @@ -0,0 +1,197 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/intmath.h> +#include <zencore/memory/align.h> +#include <zencore/memory/mallocmimalloc.h> + +#if ZEN_MIMALLOC_ENABLED + +# include <mimalloc.h> + +/** Value we fill a memory block with after it is free, in UE_BUILD_DEBUG **/ +# define DEBUG_FILL_FREED (0xdd) + +/** Value we fill a new memory block with, in UE_BUILD_DEBUG **/ +# define DEBUG_FILL_NEW (0xcd) + +# define ZEN_ENABLE_DEBUG_FILL 1 + +namespace zen { + +// Dramatically reduce memory zeroing and page faults during alloc intense workloads +// by keeping freed pages for a little while instead of releasing them +// right away to the OS, effectively acting like a scratch buffer +// until pages are both freed and inactive for the delay specified +// in milliseconds. +int32_t GMiMallocMemoryResetDelay = 10000; + +FMallocMimalloc::FMallocMimalloc() +{ + mi_option_set(mi_option_reset_delay, GMiMallocMemoryResetDelay); +} + +void* +FMallocMimalloc::TryMalloc(size_t Size, uint32_t Alignment) +{ + void* NewPtr = nullptr; + + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(uint32_t(Size >= 16 ? 16 : 8), Alignment); + NewPtr = mi_malloc_aligned(Size, Alignment); + } + else + { + NewPtr = mi_malloc_aligned(Size, uint32_t(Size >= 16 ? 16 : 8)); + } + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + if (Size && NewPtr != nullptr) + { + memset(NewPtr, DEBUG_FILL_NEW, mi_usable_size(NewPtr)); + } +# endif + + return NewPtr; +} + +void* +FMallocMimalloc::Malloc(size_t Size, uint32_t Alignment) +{ + void* Result = TryMalloc(Size, Alignment); + + if (Result == nullptr && Size) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} + +void* +FMallocMimalloc::TryRealloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + size_t OldSize = 0; + if (Ptr) + { + OldSize = mi_malloc_size(Ptr); + if (NewSize < OldSize) + { + memset((uint8_t*)Ptr + NewSize, DEBUG_FILL_FREED, OldSize - NewSize); + } + } +# endif + void* NewPtr = nullptr; + + if (NewSize == 0) + { + mi_free(Ptr); + + return nullptr; + } + +# if ZEN_PLATFORM_MAC + // macOS expects all allocations to be aligned to 16 bytes, so on Mac we always have to use mi_realloc_aligned + Alignment = AlignArbitrary(Max((uint32_t)16, Alignment), (uint32_t)16); + NewPtr = mi_realloc_aligned(Ptr, NewSize, Alignment); +# else + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(NewSize >= 16 ? (uint32_t)16 : (uint32_t)8, Alignment); + NewPtr = mi_realloc_aligned(Ptr, NewSize, Alignment); + } + else + { + NewPtr = mi_realloc(Ptr, NewSize); + } +# endif + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + if (NewPtr && NewSize > OldSize) + { + memset((uint8_t*)NewPtr + OldSize, DEBUG_FILL_NEW, mi_usable_size(NewPtr) - OldSize); + } +# endif + + return NewPtr; +} + +void* +FMallocMimalloc::Realloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ + void* Result = TryRealloc(Ptr, NewSize, Alignment); + + if (Result == nullptr && NewSize) + { + OutOfMemory(NewSize, Alignment); + } + + return Result; +} + +void +FMallocMimalloc::Free(void* Ptr) +{ + if (!Ptr) + { + return; + } + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + memset(Ptr, DEBUG_FILL_FREED, mi_usable_size(Ptr)); +# endif + + mi_free(Ptr); +} + +void* +FMallocMimalloc::MallocZeroed(size_t Size, uint32_t Alignment) +{ + void* Result = TryMallocZeroed(Size, Alignment); + + if (Result == nullptr && Size) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} + +void* +FMallocMimalloc::TryMallocZeroed(size_t Size, uint32_t Alignment) +{ + void* NewPtr = nullptr; + + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(uint32_t(Size >= 16 ? 16 : 8), Alignment); + NewPtr = mi_zalloc_aligned(Size, Alignment); + } + else + { + NewPtr = mi_zalloc_aligned(Size, uint32_t(Size >= 16 ? 16 : 8)); + } + + return NewPtr; +} + +bool +FMallocMimalloc::GetAllocationSize(void* Original, size_t& SizeOut) +{ + SizeOut = mi_malloc_size(Original); + return true; +} + +void +FMallocMimalloc::Trim(bool bTrimThreadCaches) +{ + mi_collect(bTrimThreadCaches); +} + +# undef DEBUG_FILL_FREED +# undef DEBUG_FILL_NEW + +} // namespace zen + +#endif // MIMALLOC_ENABLED diff --git a/src/zencore/memory/mallocrpmalloc.cpp b/src/zencore/memory/mallocrpmalloc.cpp new file mode 100644 index 000000000..ffced27c9 --- /dev/null +++ b/src/zencore/memory/mallocrpmalloc.cpp @@ -0,0 +1,189 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/intmath.h> +#include <zencore/memory/align.h> +#include <zencore/memory/mallocrpmalloc.h> + +#if ZEN_RPMALLOC_ENABLED + +# include "rpmalloc.h" + +/** Value we fill a memory block with after it is free, in UE_BUILD_DEBUG **/ +# define DEBUG_FILL_FREED (0xdd) + +/** Value we fill a new memory block with, in UE_BUILD_DEBUG **/ +# define DEBUG_FILL_NEW (0xcd) + +# define ZEN_ENABLE_DEBUG_FILL 1 + +namespace zen { + +FMallocRpmalloc::FMallocRpmalloc() +{ + rpmalloc_initialize(nullptr); +} + +FMallocRpmalloc::~FMallocRpmalloc() +{ + rpmalloc_finalize(); +} + +void* +FMallocRpmalloc::TryMalloc(size_t Size, uint32_t Alignment) +{ + void* NewPtr = nullptr; + + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(uint32_t(Size >= 16 ? 16 : 8), Alignment); + NewPtr = rpaligned_alloc(Alignment, Size); + } + else + { + NewPtr = rpaligned_alloc(uint32_t(Size >= 16 ? 16 : 8), Size); + } + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + if (Size && NewPtr != nullptr) + { + memset(NewPtr, DEBUG_FILL_NEW, rpmalloc_usable_size(NewPtr)); + } +# endif + + return NewPtr; +} + +void* +FMallocRpmalloc::Malloc(size_t Size, uint32_t Alignment) +{ + void* Result = TryMalloc(Size, Alignment); + + if (Result == nullptr && Size) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} + +void* +FMallocRpmalloc::Realloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ + void* Result = TryRealloc(Ptr, NewSize, Alignment); + + if (Result == nullptr && NewSize) + { + OutOfMemory(NewSize, Alignment); + } + + return Result; +} + +void* +FMallocRpmalloc::TryRealloc(void* Ptr, size_t NewSize, uint32_t Alignment) +{ +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + size_t OldSize = 0; + if (Ptr) + { + OldSize = rpmalloc_usable_size(Ptr); + if (NewSize < OldSize) + { + memset((uint8_t*)Ptr + NewSize, DEBUG_FILL_FREED, OldSize - NewSize); + } + } +# endif + void* NewPtr = nullptr; + + if (NewSize == 0) + { + rpfree(Ptr); + + return nullptr; + } + +# if ZEN_PLATFORM_MAC + // macOS expects all allocations to be aligned to 16 bytes, so on Mac we always have to use mi_realloc_aligned + Alignment = AlignArbitrary(Max((uint32_t)16, Alignment), (uint32_t)16); + NewPtr = rpaligned_realloc(Ptr, Alignment, NewSize, /* OldSize */ 0, /* flags */ 0); +# else + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(NewSize >= 16 ? (uint32_t)16 : (uint32_t)8, Alignment); + NewPtr = rpaligned_realloc(Ptr, Alignment, NewSize, /* OldSize */ 0, /* flags */ 0); + } + else + { + NewPtr = rprealloc(Ptr, NewSize); + } +# endif + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + if (NewPtr && NewSize > OldSize) + { + memset((uint8_t*)NewPtr + OldSize, DEBUG_FILL_NEW, rpmalloc_usable_size(NewPtr) - OldSize); + } +# endif + + return NewPtr; +} + +void +FMallocRpmalloc::Free(void* Ptr) +{ + if (!Ptr) + { + return; + } + +# if ZEN_BUILD_DEBUG && ZEN_ENABLE_DEBUG_FILL + memset(Ptr, DEBUG_FILL_FREED, rpmalloc_usable_size(Ptr)); +# endif + + rpfree(Ptr); +} + +void* +FMallocRpmalloc::MallocZeroed(size_t Size, uint32_t Alignment) +{ + void* Result = TryMallocZeroed(Size, Alignment); + + if (Result == nullptr && Size) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} +void* +FMallocRpmalloc::TryMallocZeroed(size_t Size, uint32_t Alignment) +{ + void* NewPtr = nullptr; + + if (Alignment != DEFAULT_ALIGNMENT) + { + Alignment = Max(uint32_t(Size >= 16 ? 16 : 8), Alignment); + NewPtr = rpaligned_zalloc(Alignment, Size); + } + else + { + NewPtr = rpaligned_zalloc(uint32_t(Size >= 16 ? 16 : 8), Size); + } + + return NewPtr; +} + +bool +FMallocRpmalloc::GetAllocationSize(void* Original, size_t& SizeOut) +{ + // this is not the same as the allocation size - is that ok? + SizeOut = rpmalloc_usable_size(Original); + return true; +} +void +FMallocRpmalloc::Trim(bool bTrimThreadCaches) +{ + ZEN_UNUSED(bTrimThreadCaches); +} +} // namespace zen +#endif diff --git a/src/zencore/memory/mallocstomp.cpp b/src/zencore/memory/mallocstomp.cpp new file mode 100644 index 000000000..db9e1535e --- /dev/null +++ b/src/zencore/memory/mallocstomp.cpp @@ -0,0 +1,283 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/memory/mallocstomp.h> + +#if ZEN_WITH_MALLOC_STOMP + +# include <zencore/memory/align.h> +# include <zencore/xxhash.h> + +# if ZEN_PLATFORM_LINUX +# include <sys/mman.h> +# endif + +# if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +# endif + +# if ZEN_PLATFORM_WINDOWS +// MallocStomp can keep virtual address range reserved after memory block is freed, while releasing the physical memory. +// This dramatically increases accuracy of use-after-free detection, but consumes significant amount of memory for the OS page table. +// Virtual memory limit for a process on Win10 is 128 TB, which means we can afford to keep virtual memory reserved for a very long time. +// Running Infiltrator demo consumes ~700MB of virtual address space per second. +# define MALLOC_STOMP_KEEP_VIRTUAL_MEMORY 1 +# else +# define MALLOC_STOMP_KEEP_VIRTUAL_MEMORY 0 +# endif + +// 64-bit ABIs on x86_64 expect a 16-byte alignment +# define STOMPALIGNMENT 16U + +namespace zen { + +struct FMallocStomp::FAllocationData +{ + /** Pointer to the full allocation. Needed so the OS knows what to free. */ + void* FullAllocationPointer; + /** Full size of the allocation including the extra page. */ + size_t FullSize; + /** Size of the allocation requested. */ + size_t Size; + /** Sentinel used to check for underrun. */ + size_t Sentinel; + + /** Calculate the expected sentinel value for this allocation data. */ + size_t CalculateSentinel() const + { + XXH3_128 Xxh = XXH3_128::HashMemory(this, offsetof(FAllocationData, Sentinel)); + + size_t Hash; + memcpy(&Hash, Xxh.Hash, sizeof(Hash)); + + return Hash; + } +}; + +FMallocStomp::FMallocStomp(const bool InUseUnderrunMode) : PageSize(4096 /* TODO: make dynamic */), bUseUnderrunMode(InUseUnderrunMode) +{ +} + +void* +FMallocStomp::Malloc(size_t Size, uint32_t Alignment) +{ + void* Result = TryMalloc(Size, Alignment); + + if (Result == nullptr) + { + OutOfMemory(Size, Alignment); + } + + return Result; +} + +void* +FMallocStomp::TryMalloc(size_t Size, uint32_t Alignment) +{ + if (Size == 0U) + { + Size = 1U; + } + + Alignment = Max<uint32_t>(Alignment, STOMPALIGNMENT); + + constexpr static size_t AllocationDataSize = sizeof(FAllocationData); + + const size_t AlignedSize = Alignment ? ((Size + Alignment - 1) & -(int32_t)Alignment) : Size; + const size_t AlignmentSize = Alignment > PageSize ? Alignment - PageSize : 0; + const size_t AllocFullPageSize = (AlignedSize + AlignmentSize + AllocationDataSize + PageSize - 1) & ~(PageSize - 1); + const size_t TotalAllocationSize = AllocFullPageSize + PageSize; + +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + void* FullAllocationPointer = mmap(nullptr, TotalAllocationSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); +# elif ZEN_PLATFORM_WINDOWS && MALLOC_STOMP_KEEP_VIRTUAL_MEMORY + // Allocate virtual address space from current block using linear allocation strategy. + // If there is not enough space, try to allocate new block from OS. Report OOM if block allocation fails. + void* FullAllocationPointer = nullptr; + + { + RwLock::ExclusiveLockScope _(Lock); + + if (VirtualAddressCursor + TotalAllocationSize <= VirtualAddressMax) + { + FullAllocationPointer = (void*)(VirtualAddressCursor); + } + else + { + const size_t ReserveSize = Max(VirtualAddressBlockSize, TotalAllocationSize); + + // Reserve a new block of virtual address space that will be linearly sub-allocated + // We intentionally don't keep track of reserved blocks, as we never need to explicitly release them. + FullAllocationPointer = VirtualAlloc(nullptr, ReserveSize, MEM_RESERVE, PAGE_NOACCESS); + + VirtualAddressCursor = uintptr_t(FullAllocationPointer); + VirtualAddressMax = VirtualAddressCursor + ReserveSize; + } + + VirtualAddressCursor += TotalAllocationSize; + } +# else + void* FullAllocationPointer = FPlatformMemory::BinnedAllocFromOS(TotalAllocationSize); +# endif // PLATFORM_UNIX || PLATFORM_MAC + + if (!FullAllocationPointer) + { + return nullptr; + } + + void* ReturnedPointer = nullptr; + + ZEN_ASSERT_SLOW(IsAligned(FullAllocationPointer, PageSize)); + + if (bUseUnderrunMode) + { + ReturnedPointer = Align((uint8_t*)FullAllocationPointer + PageSize + AllocationDataSize, Alignment); + void* AllocDataPointerStart = static_cast<FAllocationData*>(ReturnedPointer) - 1; + ZEN_ASSERT_SLOW(AllocDataPointerStart >= FullAllocationPointer); + +# if ZEN_PLATFORM_WINDOWS && MALLOC_STOMP_KEEP_VIRTUAL_MEMORY + // Commit physical pages to the used range, leaving the first page unmapped. + void* CommittedMemory = VirtualAlloc(AllocDataPointerStart, AllocationDataSize + AlignedSize, MEM_COMMIT, PAGE_READWRITE); + if (!CommittedMemory) + { + // Failed to allocate and commit physical memory pages. + return nullptr; + } + ZEN_ASSERT(CommittedMemory == AlignDown(AllocDataPointerStart, PageSize)); +# else + // Page protect the first page, this will cause the exception in case there is an underrun. + FPlatformMemory::PageProtect((uint8*)AlignDown(AllocDataPointerStart, PageSize) - PageSize, PageSize, false, false); +# endif + } //-V773 + else + { + ReturnedPointer = AlignDown((uint8_t*)FullAllocationPointer + AllocFullPageSize - AlignedSize, Alignment); + void* ReturnedPointerEnd = (uint8_t*)ReturnedPointer + AlignedSize; + ZEN_ASSERT_SLOW(IsAligned(ReturnedPointerEnd, PageSize)); + + void* AllocDataPointerStart = static_cast<FAllocationData*>(ReturnedPointer) - 1; + ZEN_ASSERT_SLOW(AllocDataPointerStart >= FullAllocationPointer); + +# if ZEN_PLATFORM_WINDOWS && MALLOC_STOMP_KEEP_VIRTUAL_MEMORY + // Commit physical pages to the used range, leaving the last page unmapped. + void* CommitPointerStart = AlignDown(AllocDataPointerStart, PageSize); + void* CommittedMemory = VirtualAlloc(CommitPointerStart, + size_t((uint8_t*)ReturnedPointerEnd - (uint8_t*)CommitPointerStart), + MEM_COMMIT, + PAGE_READWRITE); + if (!CommittedMemory) + { + // Failed to allocate and commit physical memory pages. + return nullptr; + } + ZEN_ASSERT(CommittedMemory == CommitPointerStart); +# else + // Page protect the last page, this will cause the exception in case there is an overrun. + FPlatformMemory::PageProtect(ReturnedPointerEnd, PageSize, false, false); +# endif + } //-V773 + + ZEN_ASSERT_SLOW(IsAligned(FullAllocationPointer, PageSize)); + ZEN_ASSERT_SLOW(IsAligned(TotalAllocationSize, PageSize)); + ZEN_ASSERT_SLOW(IsAligned(ReturnedPointer, Alignment)); + ZEN_ASSERT_SLOW((uint8_t*)ReturnedPointer + AlignedSize <= (uint8_t*)FullAllocationPointer + TotalAllocationSize); + + FAllocationData& AllocationData = static_cast<FAllocationData*>(ReturnedPointer)[-1]; + AllocationData = {FullAllocationPointer, TotalAllocationSize, AlignedSize, 0}; + AllocationData.Sentinel = AllocationData.CalculateSentinel(); + + return ReturnedPointer; +} + +void* +FMallocStomp::Realloc(void* InPtr, size_t NewSize, uint32_t Alignment) +{ + void* Result = TryRealloc(InPtr, NewSize, Alignment); + + if (Result == nullptr && NewSize) + { + OutOfMemory(NewSize, Alignment); + } + + return Result; +} + +void* +FMallocStomp::TryRealloc(void* InPtr, size_t NewSize, uint32_t Alignment) +{ + if (NewSize == 0U) + { + Free(InPtr); + return nullptr; + } + + void* ReturnPtr = nullptr; + + if (InPtr != nullptr) + { + ReturnPtr = TryMalloc(NewSize, Alignment); + + if (ReturnPtr != nullptr) + { + FAllocationData* AllocDataPtr = reinterpret_cast<FAllocationData*>(reinterpret_cast<uint8_t*>(InPtr) - sizeof(FAllocationData)); + memcpy(ReturnPtr, InPtr, Min(AllocDataPtr->Size, NewSize)); + Free(InPtr); + } + } + else + { + ReturnPtr = TryMalloc(NewSize, Alignment); + } + + return ReturnPtr; +} + +void +FMallocStomp::Free(void* InPtr) +{ + if (InPtr == nullptr) + { + return; + } + + FAllocationData* AllocDataPtr = reinterpret_cast<FAllocationData*>(InPtr); + AllocDataPtr--; + + // Check the sentinel to verify that the allocation data is intact. + if (AllocDataPtr->Sentinel != AllocDataPtr->CalculateSentinel()) + { + // There was a memory underrun related to this allocation. + ZEN_DEBUG_BREAK(); + } + +# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + munmap(AllocDataPtr->FullAllocationPointer, AllocDataPtr->FullSize); +# elif ZEN_PLATFORM_WINDOWS && MALLOC_STOMP_KEEP_VIRTUAL_MEMORY + // Unmap physical memory, but keep virtual address range reserved to catch use-after-free errors. + + VirtualFree(AllocDataPtr->FullAllocationPointer, AllocDataPtr->FullSize, MEM_DECOMMIT); + +# else + FPlatformMemory::BinnedFreeToOS(AllocDataPtr->FullAllocationPointer, AllocDataPtr->FullSize); +# endif // PLATFORM_UNIX || PLATFORM_MAC +} + +bool +FMallocStomp::GetAllocationSize(void* Original, size_t& SizeOut) +{ + if (Original == nullptr) + { + SizeOut = 0U; + } + else + { + FAllocationData* AllocDataPtr = reinterpret_cast<FAllocationData*>(Original); + AllocDataPtr--; + SizeOut = AllocDataPtr->Size; + } + + return true; +} + +} // namespace zen + +#endif // WITH_MALLOC_STOMP diff --git a/src/zencore/memory/memory.cpp b/src/zencore/memory/memory.cpp new file mode 100644 index 000000000..f236796ad --- /dev/null +++ b/src/zencore/memory/memory.cpp @@ -0,0 +1,281 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/commandline.h> +#include <zencore/memory/fmalloc.h> +#include <zencore/memory/mallocansi.h> +#include <zencore/memory/mallocmimalloc.h> +#include <zencore/memory/mallocrpmalloc.h> +#include <zencore/memory/mallocstomp.h> +#include <zencore/memory/memory.h> +#include <zencore/memory/memorytrace.h> +#include <zencore/string.h> + +#if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +ZEN_THIRD_PARTY_INCLUDES_START +# include <shellapi.h> // For command line parsing +ZEN_THIRD_PARTY_INCLUDES_END +#endif + +#if ZEN_PLATFORM_LINUX +# include <stdio.h> +#endif + +namespace zen { + +enum class MallocImpl +{ + None = 0, + Ansi, + Stomp, + Mimalloc, + Rpmalloc +}; + +static int +InitGMalloc() +{ + MallocImpl Malloc = MallocImpl::None; + FMalloc* InitMalloc = GMalloc; + + // Pick a default base allocator based on availability/platform + +#if ZEN_MIMALLOC_ENABLED + if (Malloc == MallocImpl::None) + { + Malloc = MallocImpl::Mimalloc; + } +#endif + +#if ZEN_RPMALLOC_ENABLED + if (Malloc == MallocImpl::None) + { + Malloc = MallocImpl::Rpmalloc; + } +#endif + + // Process any command line overrides + // + // Note that calls can come into this function before we enter the regular main function + // and we can therefore not rely on the regular command line parsing for the application + + using namespace std::literals; + + auto ProcessMallocArg = [&](const std::string_view& Arg) { +#if ZEN_RPMALLOC_ENABLED + if (Arg == "rpmalloc"sv) + { + Malloc = MallocImpl::Rpmalloc; + } +#endif + +#if ZEN_MIMALLOC_ENABLED + if (Arg == "mimalloc"sv) + { + Malloc = MallocImpl::Mimalloc; + } +#endif + + if (Arg == "ansi"sv) + { + Malloc = MallocImpl::Ansi; + } + + if (Arg == "stomp"sv) + { + Malloc = MallocImpl::Stomp; + } + }; + + constexpr std::string_view MallocOption = "--malloc="sv; + + std::function<void(const std::string_view&)> ProcessArg = [&](const std::string_view& Arg) { + if (Arg.starts_with(MallocOption)) + { + const std::string_view OptionArgs = Arg.substr(MallocOption.size()); + + IterateCommaSeparatedValue(OptionArgs, ProcessMallocArg); + } + }; + + IterateCommandlineArgs(ProcessArg); + + switch (Malloc) + { +#if ZEN_WITH_MALLOC_STOMP + case MallocImpl::Stomp: + GMalloc = new FMallocStomp(); + break; +#endif + +#if ZEN_RPMALLOC_ENABLED + case MallocImpl::Rpmalloc: + GMalloc = new FMallocRpmalloc(); + break; +#endif + +#if ZEN_MIMALLOC_ENABLED + case MallocImpl::Mimalloc: + GMalloc = new FMallocMimalloc(); + break; +#endif + default: + break; + } + + if (GMalloc == InitMalloc) + { + GMalloc = new FMallocAnsi(); + } + + return 1; +} + +void +Memory::GCreateMalloc() +{ + static int InitFlag = InitGMalloc(); +} + +void +Memory::Initialize() +{ + GCreateMalloc(); +} + +////////////////////////////////////////////////////////////////////////// + +void* +Memory::SystemMalloc(size_t Size) +{ + void* Ptr = ::malloc(Size); + MemoryTrace_Alloc(uint64_t(Ptr), Size, 0, EMemoryTraceRootHeap::SystemMemory); + return Ptr; +} + +void +Memory::SystemFree(void* Ptr) +{ + MemoryTrace_Free(uint64_t(Ptr), EMemoryTraceRootHeap::SystemMemory); + ::free(Ptr); +} + +} // namespace zen + +////////////////////////////////////////////////////////////////////////// + +static ZEN_NOINLINE bool +InvokeNewHandler(bool NoThrow) +{ + std::new_handler h = std::get_new_handler(); + + if (!h) + { +#if defined(_CPPUNWIND) || defined(__cpp_exceptions) + if (NoThrow == false) + throw std::bad_alloc(); +#else + ZEN_UNUSED(NoThrow); +#endif + return false; + } + else + { + h(); + return true; + } +} + +////////////////////////////////////////////////////////////////////////// + +ZEN_NOINLINE void* +RetryNew(size_t Size, bool NoThrow) +{ + void* Ptr = nullptr; + while (!Ptr && InvokeNewHandler(NoThrow)) + { + Ptr = zen::Memory::Malloc(Size, zen::DEFAULT_ALIGNMENT); + } + return Ptr; +} + +void* +zen_new(size_t Size) +{ + void* Ptr = zen::Memory::Malloc(Size, zen::DEFAULT_ALIGNMENT); + + if (!Ptr) [[unlikely]] + { + const bool NoThrow = false; + return RetryNew(Size, NoThrow); + } + + return Ptr; +} + +void* +zen_new_nothrow(size_t Size) noexcept +{ + void* Ptr = zen::Memory::Malloc(Size, zen::DEFAULT_ALIGNMENT); + + if (!Ptr) [[unlikely]] + { + const bool NoThrow = true; + return RetryNew(Size, NoThrow); + } + + return Ptr; +} + +void* +zen_new_aligned(size_t Size, size_t Alignment) +{ + void* Ptr; + + do + { + Ptr = zen::Memory::Malloc(Size, uint32_t(Alignment)); + } while (!Ptr && InvokeNewHandler(/* NoThrow */ false)); + + return Ptr; +} + +void* +zen_new_aligned_nothrow(size_t Size, size_t Alignment) noexcept +{ + void* Ptr; + + do + { + Ptr = zen::Memory::Malloc(Size, uint32_t(Alignment)); + } while (!Ptr && InvokeNewHandler(/* NoThrow */ true)); + + return Ptr; +} + +void +zen_free(void* Ptr) noexcept +{ + zen::Memory::Free(Ptr); +} + +void +zen_free_size(void* Ptr, size_t Size) noexcept +{ + ZEN_UNUSED(Size); + zen::Memory::Free(Ptr); +} + +void +zen_free_size_aligned(void* Ptr, size_t Size, size_t Alignment) noexcept +{ + ZEN_UNUSED(Size, Alignment); + zen::Memory::Free(Ptr); +} + +void +zen_free_aligned(void* Ptr, size_t Alignment) noexcept +{ + ZEN_UNUSED(Alignment); + zen::Memory::Free(Ptr); +} |