Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
paged_allocator.h
1#pragma once
2#include "gaia/config/config.h"
3
4#include <cinttypes>
5#include <cstdint>
6#include <cstring>
7#include <type_traits>
8
9#include "gaia/cnt/fwd_llist.h"
10#include "gaia/cnt/sarray.h"
11#include "gaia/core/bit_utils.h"
12#include "gaia/core/dyn_singleton.h"
13#include "gaia/core/utility.h"
14#include "gaia/mem/mem_alloc.h"
15#include "gaia/meta/type_info.h"
16#include "gaia/util/logging.h"
17
18namespace gaia {
19 namespace mem {
21 static constexpr uint32_t MemoryBlockAlignment = 16;
24 static constexpr uint32_t MemoryBlockBytesDefault = 32768;
26 static constexpr uint32_t MemoryBlockUsableOffset = sizeof(uintptr_t);
27
29 struct GAIA_API MemoryPageHeader {
31 void* m_data;
32
35 MemoryPageHeader(void* ptr): m_data(ptr) {}
36 };
37
41 template <typename T, uint32_t RequestedBlockSize>
42 struct MemoryPage: MemoryPageHeader, cnt::fwd_llist_base<MemoryPage<T, RequestedBlockSize>> {
46 static constexpr uint32_t next_multiple_of_alignment(uint32_t num) {
47 return (num + (MemoryBlockAlignment - 1)) & uint32_t(-(int32_t)MemoryBlockAlignment);
48 }
51 static constexpr uint32_t calculate_block_size() {
52 if constexpr (RequestedBlockSize == 0)
53 return next_multiple_of_alignment(MemoryBlockBytesDefault);
54 else
55 return next_multiple_of_alignment(RequestedBlockSize);
56 }
57
59 static constexpr uint32_t MemoryBlockBytes = calculate_block_size();
61 static constexpr uint16_t NBlocks = 48;
63 static constexpr uint16_t NBlocks_Bits = (uint16_t)core::count_bits(NBlocks);
65 static constexpr uint32_t InvalidBlockId = NBlocks + 1;
66#if GAIA_DEBUG
67 static constexpr uint8_t FreedBlockPattern = 0xDD;
68 static constexpr uintptr_t FreedPageMarker = ~(uintptr_t)0;
69#endif
71 static constexpr uint32_t BlockArrayBytes = ((uint32_t)NBlocks_Bits * (uint32_t)NBlocks + 7) / 8;
72
79
82
92 // uint32_t m_unused : 8;
93
96 MemoryPage(void* ptr):
98 // One cacheline long on x86. The point is for this to be as small as possible
99 static_assert(sizeof(MemoryPage) <= 64);
100 }
101
105 void write_block_idx(uint32_t blockIdx, uint32_t value) {
106 const uint32_t bitPosition = blockIdx * NBlocks_Bits;
107
108 GAIA_ASSERT(bitPosition < NBlocks * NBlocks_Bits);
109 GAIA_ASSERT(value <= InvalidBlockId);
110
111 BitView{{(uint8_t*)m_blocks.data(), BlockArrayBytes}}.set(bitPosition, (uint8_t)value);
112 }
113
117 uint8_t read_block_idx(uint32_t blockIdx) const {
118 const uint32_t bitPosition = blockIdx * NBlocks_Bits;
119
120 GAIA_ASSERT(bitPosition < NBlocks * NBlocks_Bits);
121
122 return BitView{{(uint8_t*)m_blocks.data(), BlockArrayBytes}}.get(bitPosition);
123 }
124
127 GAIA_NODISCARD void* alloc_block() {
128 auto StoreBlockAddress = [&](uint32_t index) {
129 // Encode info about the block's page in the memory block.
130 // The actual pointer returned is offset by MemoryBlockUsableOffset bytes
131 uint8_t* pMemoryBlock = (uint8_t*)m_data + (index * MemoryBlockBytes);
132 GAIA_ASSERT((uintptr_t)pMemoryBlock % MemoryBlockAlignment == 0);
133 mem::unaligned_ref<uintptr_t>{pMemoryBlock} = (uintptr_t)this;
134 return (void*)(pMemoryBlock + MemoryBlockUsableOffset);
135 };
136
137 // We don't want to go out of range for new blocks
138 GAIA_ASSERT(!full() && "Trying to allocate too many blocks!");
139
140 if (m_freeBlocks == 0U) {
141 const auto index = m_blockCnt;
142 ++m_usedBlocks;
143 ++m_blockCnt;
144 write_block_idx(index, index);
145
146 return StoreBlockAddress(index);
147 }
148
149 GAIA_ASSERT(m_nextFreeBlock < m_blockCnt && "Block allocator recycle list broken!");
150
151 ++m_usedBlocks;
152 --m_freeBlocks;
153
154 const auto index = m_nextFreeBlock;
156
157 return StoreBlockAddress(index);
158 }
159
162 void free_block(void* pBlock) {
163 GAIA_ASSERT(m_usedBlocks > 0);
164 GAIA_ASSERT(m_freeBlocks <= NBlocks);
165
166 auto ReadBlockAddress = [&](void* pMemory) {
167 // Offset the chunk memory so we get the real block address
168 const auto* pMemoryBlock = (uint8_t*)pMemory - MemoryBlockUsableOffset;
169#if GAIA_ASSERT_ENABLED
170 const auto pageAddr = (uintptr_t)mem::unaligned_ref<uintptr_t>{(void*)pMemoryBlock};
171 GAIA_ASSERT(pageAddr == (uintptr_t)this);
172#endif
173 const auto blckAddr = (uintptr_t)pMemoryBlock;
174 GAIA_ASSERT(blckAddr % 16 == 0);
175 const auto dataAddr = (uintptr_t)m_data;
176 const auto blockIdx = (uint32_t)((blckAddr - dataAddr) / MemoryBlockBytes);
177 return blockIdx;
178 };
179 const auto blockIdx = ReadBlockAddress(pBlock);
180
181#if GAIA_DEBUG
182 mem::unaligned_ref<uintptr_t>{(uint8_t*)pBlock - MemoryBlockUsableOffset} = FreedPageMarker;
183 std::memset(pBlock, FreedBlockPattern, MemoryBlockBytes - MemoryBlockUsableOffset);
184#endif
185
186 // Update our implicit list
187 if (m_freeBlocks == 0U)
189 else
191 m_nextFreeBlock = blockIdx;
192
193 ++m_freeBlocks;
194 --m_usedBlocks;
195 }
196
199 GAIA_NODISCARD uint32_t used_blocks_cnt() const {
200 return m_usedBlocks;
201 }
202
205 GAIA_NODISCARD bool full() const {
206 return used_blocks_cnt() >= NBlocks;
207 }
208
211 GAIA_NODISCARD bool empty() const {
212 return used_blocks_cnt() == 0;
213 }
214
216 void verify() const {
217#if GAIA_ASSERT_ENABLED
218 GAIA_ASSERT(m_blockCnt <= NBlocks);
219 GAIA_ASSERT(m_usedBlocks <= m_blockCnt);
220 GAIA_ASSERT(m_freeBlocks <= m_blockCnt);
221 GAIA_ASSERT(m_usedBlocks + m_freeBlocks == m_blockCnt);
222 GAIA_ASSERT(((uintptr_t)m_data % MemoryBlockAlignment) == 0);
223
224 uint64_t freeMask = 0;
225 if (m_freeBlocks != 0) {
226 uint32_t next = m_nextFreeBlock;
227 GAIA_FOR(m_freeBlocks) {
228 GAIA_ASSERT(next < m_blockCnt);
229 const auto bit = uint64_t(1) << next;
230 GAIA_ASSERT((freeMask & bit) == 0 && "Free list contains a cycle");
231 freeMask |= bit;
232 next = read_block_idx(next);
233 }
234
235 GAIA_ASSERT(next == InvalidBlockId);
236 }
237
238 GAIA_FOR(m_blockCnt) {
239 const auto* pMemoryBlock = (const uint8_t*)m_data + (i * MemoryBlockBytes);
240 GAIA_ASSERT(((uintptr_t)pMemoryBlock % MemoryBlockAlignment) == 0);
241
242 #if GAIA_DEBUG
243 const bool isFree = (freeMask & (uint64_t(1) << i)) != 0;
244 const auto pageAddr = (uintptr_t)mem::unaligned_ref<uintptr_t>{(void*)pMemoryBlock};
245 GAIA_ASSERT(pageAddr == (isFree ? FreedPageMarker : (uintptr_t)this));
246 #endif
247 }
248#endif
249 }
250 };
251
255 template <typename T, uint32_t RequestedBlockSize>
268
270 struct GAIA_API MemoryPageStats final {
272 uint64_t mem_total;
274 uint64_t mem_used;
276 uint32_t num_pages;
279 };
280
282 namespace detail {
283 template <typename T, uint32_t RequestedBlockSize>
284 class PagedAllocatorImpl;
285 }
287
291 template <typename T, uint32_t RequestedBlockSize = 0>
293
295 namespace detail {
296
297 template <typename T, uint32_t RequestedBlockSize>
298 class PagedAllocatorImpl {
299 friend ::gaia::mem::PagedAllocator<T, RequestedBlockSize>;
300
301 inline static char s_strPageData[256]{};
302 inline static char s_strMemPage[256]{};
303
304 using Page = MemoryPage<T, RequestedBlockSize>;
305 using PageContainer = MemoryPageContainer<T, RequestedBlockSize>;
306
308 PageContainer m_pages;
310 bool m_isDone = false;
311
312 private:
313 PagedAllocatorImpl() {
314 // PagedAllocatorImpl is only used as a singleton so the constructor is going to be called just once.
315 // Therefore, the strings are only going to be set once.
316 auto ct_name = meta::type_info::name<T>();
317 const auto ct_name_len = (uint32_t)ct_name.size();
318 GAIA_STRCPY(s_strPageData, 256, "PageData_");
319 memcpy((void*)&s_strPageData[9], (const void*)ct_name.data(), ct_name_len);
320 s_strPageData[9 + ct_name_len] = 0;
321 GAIA_STRCPY(s_strMemPage, 256, "MemPage_");
322 memcpy((void*)&s_strMemPage[8], (const void*)ct_name.data(), ct_name_len);
323 s_strMemPage[8 + ct_name_len] = 0;
324 }
325
326 void on_delete() {
327 flush();
328
329 // Make sure there are no leaks
330 auto memStats = stats();
331 if (memStats.mem_total != 0) {
332 GAIA_ASSERT2(false, "Paged allocator leaking memory");
333 GAIA_LOG_W("Paged allocator leaking memory!");
334 diag();
335 }
336 }
337
338 public:
339 ~PagedAllocatorImpl() {
340 on_delete();
341 }
342
343 PagedAllocatorImpl(PagedAllocatorImpl&& world) = delete;
344 PagedAllocatorImpl(const PagedAllocatorImpl& world) = delete;
345 PagedAllocatorImpl& operator=(PagedAllocatorImpl&&) = delete;
346 PagedAllocatorImpl& operator=(const PagedAllocatorImpl&) = delete;
347
349 void* alloc([[maybe_unused]] uint32_t dummy) {
350 const detail::ArenaLock arenaLock;
351 void* pBlock = nullptr;
352
353 // Find first page with available space
354 auto* pPage = m_pages.pagesFree.first;
355 GAIA_ASSERT(pPage == nullptr || !pPage->full());
356 if (pPage == nullptr) {
357 // Allocate a new page if no free page was found
358 pPage = alloc_page();
359 m_pages.pagesFree.link(pPage);
360 }
361
362 // Allocate a new chunk of memory
363 pBlock = pPage->alloc_block();
364
365 // Handle full pages
366 if (pPage->full()) {
367 // Remove the page from the open list
368 m_pages.pagesFree.unlink(pPage);
369 // Move our page to the full list
370 m_pages.pagesFull.link(pPage);
371 }
372
373 verify();
374 return pBlock;
375 }
376
377 GAIA_CLANG_WARNING_PUSH()
378 // Memory is aligned so we can silence this warning
379 GAIA_CLANG_WARNING_DISABLE("-Wcast-align")
380
382 void free(void* pBlock) {
383 const detail::ArenaLock arenaLock;
384 // Decode the page from the address
385 const auto pageAddr = *(uintptr_t*)((uint8_t*)pBlock - MemoryBlockUsableOffset);
386 GAIA_ASSERT(pageAddr % MemoryBlockAlignment == 0);
387 auto* pPage = (Page*)pageAddr;
388 const bool wasFull = pPage->full();
389
390#if GAIA_ASSERT_ENABLED
391 if (wasFull) {
392 const auto res = m_pages.pagesFull.has(pPage);
393 GAIA_ASSERT(res && "Memory page couldn't be found among full pages");
394 } else {
395 const auto res = m_pages.pagesFree.has(pPage);
396 GAIA_ASSERT(res && "Memory page couldn't be found among free pages");
397 }
398#endif
399
400 // Free the chunk
401 pPage->free_block(pBlock);
402
403 // Update lists
404 if (wasFull) {
405 // Our page is no longer full
406 m_pages.pagesFull.unlink(pPage);
407 // Move our page to the open list
408 m_pages.pagesFree.link(pPage);
409 }
410
411 verify();
412
413 // Special handling for the allocator signaled to destroy itself
414 if (m_isDone) {
415 // Remove the page right away
416 if (pPage->empty()) {
417 GAIA_ASSERT(!m_pages.pagesFree.empty());
418 m_pages.pagesFree.unlink(pPage);
419 }
420
421 try_delete_this();
422 }
423 }
424
425 GAIA_CLANG_WARNING_POP()
426
427
428 MemoryPageStats stats() const {
429 MemoryPageStats stats{};
430
431 stats.num_pages = (uint32_t)m_pages.pagesFree.size() + (uint32_t)m_pages.pagesFull.size();
432 stats.num_pages_free = (uint32_t)m_pages.pagesFree.size();
433 stats.mem_total = stats.num_pages * (size_t)Page::MemoryBlockBytes * Page::NBlocks;
434 stats.mem_used = m_pages.pagesFull.size() * (size_t)Page::MemoryBlockBytes * Page::NBlocks;
435 for (const auto& page: m_pages.pagesFree)
436 stats.mem_used += page.used_blocks_cnt() * (size_t)Page::MemoryBlockBytes;
437
438 return stats;
439 }
440
442 void flush() {
443 const detail::ArenaLock arenaLock;
444 for (auto it = m_pages.pagesFree.begin(); it != m_pages.pagesFree.end();) {
445 auto* pPage = &(*it);
446 ++it;
447
448 // Skip non-empty pages
449 if (!pPage->empty())
450 continue;
451
452 m_pages.pagesFree.unlink(pPage);
453 free_page(pPage);
454 }
455
456 verify();
457 }
458
460 void diag() const {
461 auto memStats = stats();
462 GAIA_LOG_N("PagedAllocator %p stats", (void*)this);
463 GAIA_LOG_N(" Allocated: %" PRIu64 " B", memStats.mem_total);
464 GAIA_LOG_N(" Used: %" PRIu64 " B", memStats.mem_total - memStats.mem_used);
465 GAIA_LOG_N(" Overhead: %" PRIu64 " B", memStats.mem_used);
466 GAIA_LOG_N(
467 " Utilization: %.1f%%",
468 memStats.mem_total != 0 ? 100.0 * ((double)memStats.mem_used / (double)memStats.mem_total) : 0.0);
469 GAIA_LOG_N(" Pages: %u", memStats.num_pages);
470 GAIA_LOG_N(" Free pages: %u", memStats.num_pages_free);
471 }
472
473 void verify() const {
474#if GAIA_ASSERT_ENABLED
475 for (const auto& page: m_pages.pagesFree) {
476 GAIA_ASSERT(page.get_fwd_llist_link().linked());
477 GAIA_ASSERT(!page.full());
478 page.verify();
479 }
480
481 for (const auto& page: m_pages.pagesFull) {
482 GAIA_ASSERT(page.get_fwd_llist_link().linked());
483 GAIA_ASSERT(page.full());
484 page.verify();
485 }
486#endif
487 }
488
489 private:
490 static Page* alloc_page() {
491 const uint32_t size = Page::NBlocks * Page::MemoryBlockBytes;
492 auto* pPageData = mem::AllocHelper::alloc_alig<uint8_t>(&s_strPageData[0], MemoryBlockAlignment, size);
493 auto* pMemoryPage = mem::AllocHelper::alloc<Page>(&s_strMemPage[0]);
494 return new (pMemoryPage) Page(pPageData);
495 }
496
497 static void free_page(Page* pMemoryPage) {
498 GAIA_ASSERT(pMemoryPage != nullptr);
499
500 mem::AllocHelper::free_alig(&s_strPageData[0], pMemoryPage->m_data);
501 pMemoryPage->~MemoryPage();
502 mem::AllocHelper::free(&s_strMemPage[0], pMemoryPage);
503 }
504
505 void done() {
506 m_isDone = true;
507 }
508
509 void try_delete_this() {
510 // When there is nothing left, delete the allocator
511 if (m_pages.empty())
512 delete this;
513 }
514 };
515
516 } // namespace detail
518 } // namespace mem
519} // namespace gaia
Array with variable size of elements of type.
Definition darray_impl.h:27
GAIA_NODISCARD bool empty() const noexcept
Checks whether the container has no elements.
Definition darray_impl.h:510
GAIA_NODISCARD pointer data() noexcept
Returns a pointer to the element storage.
Definition darray_impl.h:193
Gaia-ECS is a header-only library which means we want to avoid using global static variables because ...
Definition dyn_singleton.h:29
Pointer wrapper for writing memory in defined way (not causing undefined behavior)
Definition mem_alloc.h:423
Each fwd_llist node either has to inherit from fwd_llist_base or it has to provide get_fwd_llist_link...
Definition fwd_llist.h:29
Provides packed access to fixed-width unsigned values stored in a byte span.
Definition bit_utils.h:13
Lists the non-full and full pages belonging to an allocator.
Definition paged_allocator.h:256
cnt::fwd_llist< MemoryPage< T, RequestedBlockSize > > pagesFull
List of full pages.
Definition paged_allocator.h:260
GAIA_NODISCARD bool empty() const
Reports whether the container has no pages.
Definition paged_allocator.h:264
cnt::fwd_llist< MemoryPage< T, RequestedBlockSize > > pagesFree
List of available pages.
Definition paged_allocator.h:258
Common header for allocator pages.
Definition paged_allocator.h:29
void * m_data
Pointer to data managed by page.
Definition paged_allocator.h:31
MemoryPageHeader(void *ptr)
Creates a page header for a backing allocation.
Definition paged_allocator.h:35
Aggregate statistics for a paged allocator.
Definition paged_allocator.h:270
uint32_t num_pages_free
Number of free pages.
Definition paged_allocator.h:278
uint64_t mem_used
Memory actively used.
Definition paged_allocator.h:274
uint64_t mem_total
Total allocated memory.
Definition paged_allocator.h:272
uint32_t num_pages
Number of allocated pages.
Definition paged_allocator.h:276
Fixed-capacity page of equal-sized blocks.
Definition paged_allocator.h:42
uint8_t read_block_idx(uint32_t blockIdx) const
Reads one link from the packed recycled-block list.
Definition paged_allocator.h:117
static constexpr uint16_t NBlocks
Maximum number of blocks in a page.
Definition paged_allocator.h:61
void free_block(void *pBlock)
Release the block allocated by this page.
Definition paged_allocator.h:162
void write_block_idx(uint32_t blockIdx, uint32_t value)
Writes one link in the packed recycled-block list.
Definition paged_allocator.h:105
BlockArray m_blocks
Implicit list of blocks.
Definition paged_allocator.h:81
static constexpr uint32_t calculate_block_size()
Selects and aligns the configured block size.
Definition paged_allocator.h:51
uint32_t m_nextFreeBlock
Index of the next block to recycle.
Definition paged_allocator.h:88
void verify() const
Verifies page invariants in assertion-enabled builds.
Definition paged_allocator.h:216
uint32_t m_freeBlocks
Number of blocks to recycle.
Definition paged_allocator.h:90
GAIA_NODISCARD bool empty() const
Reports whether no page blocks are allocated.
Definition paged_allocator.h:211
GAIA_NODISCARD bool full() const
Reports whether all page blocks are allocated.
Definition paged_allocator.h:205
uint32_t m_usedBlocks
Number of used blocks out of NBlocks.
Definition paged_allocator.h:86
static constexpr uint32_t BlockArrayBytes
Bytes occupied by the packed block-index array.
Definition paged_allocator.h:71
static constexpr uint32_t MemoryBlockBytes
Size of one block in bytes.
Definition paged_allocator.h:59
MemoryPage(void *ptr)
Free bits to use in the future.
Definition paged_allocator.h:96
uint32_t m_blockCnt
Number of blocks in the block array.
Definition paged_allocator.h:84
GAIA_NODISCARD uint32_t used_blocks_cnt() const
Returns the number of live blocks.
Definition paged_allocator.h:199
static constexpr uint32_t InvalidBlockId
Sentinel terminating the recycled-block list.
Definition paged_allocator.h:65
static constexpr uint16_t NBlocks_Bits
Bits required to encode a block index.
Definition paged_allocator.h:63
GAIA_NODISCARD void * alloc_block()
Allocate a new block for this page.
Definition paged_allocator.h:127
static constexpr uint32_t next_multiple_of_alignment(uint32_t num)
Rounds a size up to MemoryBlockAlignment.
Definition paged_allocator.h:46