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 = MemoryBlockAlignment;
27 static_assert(MemoryBlockUsableOffset >= sizeof(uintptr_t));
28
30 struct GAIA_API MemoryPageHeader {
32 void* m_data;
33
36 MemoryPageHeader(void* ptr): m_data(ptr) {}
37 };
38
42 template <typename T, uint32_t RequestedBlockSize>
43 struct MemoryPage: MemoryPageHeader, cnt::fwd_llist_base<MemoryPage<T, RequestedBlockSize>> {
47 static constexpr uint32_t next_multiple_of_alignment(uint32_t num) {
48 return (num + (MemoryBlockAlignment - 1)) & uint32_t(-(int32_t)MemoryBlockAlignment);
49 }
52 static constexpr uint32_t calculate_block_size() {
53 constexpr uint32_t UsableBytes = RequestedBlockSize == 0 ? MemoryBlockBytesDefault : RequestedBlockSize;
54 return next_multiple_of_alignment(UsableBytes + MemoryBlockUsableOffset);
55 }
56
58 static constexpr uint32_t MemoryBlockBytes = calculate_block_size();
60 static constexpr uint16_t NBlocks = 48;
62 static constexpr uint16_t NBlocks_Bits = (uint16_t)core::count_bits(NBlocks);
64 static constexpr uint32_t InvalidBlockId = NBlocks + 1;
65#if GAIA_DEBUG
66 static constexpr uint8_t FreedBlockPattern = 0xDD;
67 static constexpr uintptr_t FreedPageMarker = ~(uintptr_t)0;
68#endif
70 static constexpr uint32_t BlockArrayBytes = ((uint32_t)NBlocks_Bits * (uint32_t)NBlocks + 7) / 8;
71
78
81
91 // uint32_t m_unused : 8;
92
95 MemoryPage(void* ptr):
97 // One cacheline long on x86. The point is for this to be as small as possible
98 static_assert(sizeof(MemoryPage) <= 64);
99 }
100
104 void write_block_idx(uint32_t blockIdx, uint32_t value) {
105 const uint32_t bitPosition = blockIdx * NBlocks_Bits;
106
107 GAIA_ASSERT(bitPosition < NBlocks * NBlocks_Bits);
108 GAIA_ASSERT(value <= InvalidBlockId);
109
110 BitView{{(uint8_t*)m_blocks.data(), BlockArrayBytes}}.set(bitPosition, (uint8_t)value);
111 }
112
116 uint8_t read_block_idx(uint32_t blockIdx) const {
117 const uint32_t bitPosition = blockIdx * NBlocks_Bits;
118
119 GAIA_ASSERT(bitPosition < NBlocks * NBlocks_Bits);
120
121 return BitView{{(uint8_t*)m_blocks.data(), BlockArrayBytes}}.get(bitPosition);
122 }
123
126 GAIA_NODISCARD void* alloc_block() {
127 auto StoreBlockAddress = [&](uint32_t index) {
128 // Encode info about the block's page in the memory block.
129 // The actual pointer returned is offset by MemoryBlockUsableOffset bytes
130 uint8_t* pMemoryBlock = (uint8_t*)m_data + (index * MemoryBlockBytes);
131 GAIA_ASSERT((uintptr_t)pMemoryBlock % MemoryBlockAlignment == 0);
132 mem::unaligned_ref<uintptr_t>{pMemoryBlock} = (uintptr_t)this;
133 return (void*)(pMemoryBlock + MemoryBlockUsableOffset);
134 };
135
136 // We don't want to go out of range for new blocks
137 GAIA_ASSERT(!full() && "Trying to allocate too many blocks!");
138
139 if (m_freeBlocks == 0U) {
140 const auto index = m_blockCnt;
141 ++m_usedBlocks;
142 ++m_blockCnt;
143 write_block_idx(index, index);
144
145 return StoreBlockAddress(index);
146 }
147
148 GAIA_ASSERT(m_nextFreeBlock < m_blockCnt && "Block allocator recycle list broken!");
149
150 ++m_usedBlocks;
151 --m_freeBlocks;
152
153 const auto index = m_nextFreeBlock;
155
156 return StoreBlockAddress(index);
157 }
158
161 void free_block(void* pBlock) {
162 GAIA_ASSERT(m_usedBlocks > 0);
163 GAIA_ASSERT(m_freeBlocks <= NBlocks);
164
165 auto ReadBlockAddress = [&](void* pMemory) {
166 // Offset the chunk memory so we get the real block address
167 const auto* pMemoryBlock = (uint8_t*)pMemory - MemoryBlockUsableOffset;
168#if GAIA_ASSERT_ENABLED
169 const auto pageAddr = (uintptr_t)mem::unaligned_ref<uintptr_t>{(void*)pMemoryBlock};
170 GAIA_ASSERT(pageAddr == (uintptr_t)this);
171#endif
172 const auto blckAddr = (uintptr_t)pMemoryBlock;
173 GAIA_ASSERT(blckAddr % 16 == 0);
174 const auto dataAddr = (uintptr_t)m_data;
175 const auto blockIdx = (uint32_t)((blckAddr - dataAddr) / MemoryBlockBytes);
176 return blockIdx;
177 };
178 const auto blockIdx = ReadBlockAddress(pBlock);
179
180#if GAIA_DEBUG
181 mem::unaligned_ref<uintptr_t>{(uint8_t*)pBlock - MemoryBlockUsableOffset} = FreedPageMarker;
182 std::memset(pBlock, FreedBlockPattern, MemoryBlockBytes - MemoryBlockUsableOffset);
183#endif
184
185 // Update our implicit list
186 if (m_freeBlocks == 0U)
188 else
190 m_nextFreeBlock = blockIdx;
191
192 ++m_freeBlocks;
193 --m_usedBlocks;
194 }
195
198 GAIA_NODISCARD uint32_t used_blocks_cnt() const {
199 return m_usedBlocks;
200 }
201
204 GAIA_NODISCARD bool full() const {
205 return used_blocks_cnt() >= NBlocks;
206 }
207
210 GAIA_NODISCARD bool empty() const {
211 return used_blocks_cnt() == 0;
212 }
213
215 void verify() const {
216#if GAIA_ASSERT_ENABLED
217 GAIA_ASSERT(m_blockCnt <= NBlocks);
218 GAIA_ASSERT(m_usedBlocks <= m_blockCnt);
219 GAIA_ASSERT(m_freeBlocks <= m_blockCnt);
220 GAIA_ASSERT(m_usedBlocks + m_freeBlocks == m_blockCnt);
221 GAIA_ASSERT(((uintptr_t)m_data % MemoryBlockAlignment) == 0);
222
223 uint64_t freeMask = 0;
224 if (m_freeBlocks != 0) {
225 uint32_t next = m_nextFreeBlock;
226 GAIA_FOR(m_freeBlocks) {
227 GAIA_ASSERT(next < m_blockCnt);
228 const auto bit = uint64_t(1) << next;
229 GAIA_ASSERT((freeMask & bit) == 0 && "Free list contains a cycle");
230 freeMask |= bit;
231 next = read_block_idx(next);
232 }
233
234 GAIA_ASSERT(next == InvalidBlockId);
235 }
236
237 GAIA_FOR(m_blockCnt) {
238 const auto* pMemoryBlock = (const uint8_t*)m_data + (i * MemoryBlockBytes);
239 GAIA_ASSERT(((uintptr_t)pMemoryBlock % MemoryBlockAlignment) == 0);
240
241 #if GAIA_DEBUG
242 const bool isFree = (freeMask & (uint64_t(1) << i)) != 0;
243 const auto pageAddr = (uintptr_t)mem::unaligned_ref<uintptr_t>{(void*)pMemoryBlock};
244 GAIA_ASSERT(pageAddr == (isFree ? FreedPageMarker : (uintptr_t)this));
245 #endif
246 }
247#endif
248 }
249 };
250
254 template <typename T, uint32_t RequestedBlockSize>
267
269 struct GAIA_API MemoryPageStats final {
271 uint64_t mem_total;
273 uint64_t mem_used;
275 uint32_t num_pages;
278 };
279
281 namespace detail {
282 template <typename T, uint32_t RequestedBlockSize>
283 class PagedAllocatorImpl;
284 }
286
290 template <typename T, uint32_t RequestedBlockSize = 0>
292
294 namespace detail {
295
296 template <typename T, uint32_t RequestedBlockSize>
297 class PagedAllocatorImpl {
298 friend ::gaia::mem::PagedAllocator<T, RequestedBlockSize>;
299
300 inline static char s_strPageData[256]{};
301 inline static char s_strMemPage[256]{};
302
303 using Page = MemoryPage<T, RequestedBlockSize>;
304 using PageContainer = MemoryPageContainer<T, RequestedBlockSize>;
305
307 PageContainer m_pages;
309 bool m_isDone = false;
310
311 private:
312 PagedAllocatorImpl() {
313 // PagedAllocatorImpl is only used as a singleton so the constructor is going to be called just once.
314 // Therefore, the strings are only going to be set once.
315 auto ct_name = meta::type_info::name<T>();
316 const auto ct_name_len = (uint32_t)ct_name.size();
317 GAIA_STRCPY(s_strPageData, 256, "PageData_");
318 memcpy((void*)&s_strPageData[9], (const void*)ct_name.data(), ct_name_len);
319 s_strPageData[9 + ct_name_len] = 0;
320 GAIA_STRCPY(s_strMemPage, 256, "MemPage_");
321 memcpy((void*)&s_strMemPage[8], (const void*)ct_name.data(), ct_name_len);
322 s_strMemPage[8 + ct_name_len] = 0;
323 }
324
325 void on_delete() {
326 flush();
327
328 // Make sure there are no leaks
329 auto memStats = stats();
330 if (memStats.mem_total != 0) {
331 GAIA_ASSERT2(false, "Paged allocator leaking memory");
332 GAIA_LOG_W("Paged allocator leaking memory!");
333 diag();
334 }
335 }
336
337 public:
338 ~PagedAllocatorImpl() {
339 on_delete();
340 }
341
342 PagedAllocatorImpl(PagedAllocatorImpl&& world) = delete;
343 PagedAllocatorImpl(const PagedAllocatorImpl& world) = delete;
344 PagedAllocatorImpl& operator=(PagedAllocatorImpl&&) = delete;
345 PagedAllocatorImpl& operator=(const PagedAllocatorImpl&) = delete;
346
348 void* alloc([[maybe_unused]] uint32_t dummy) {
349 const detail::ArenaLock arenaLock;
350 void* pBlock = nullptr;
351
352 // Find first page with available space
353 auto* pPage = m_pages.pagesFree.first;
354 GAIA_ASSERT(pPage == nullptr || !pPage->full());
355 if (pPage == nullptr) {
356 // Allocate a new page if no free page was found
357 pPage = alloc_page();
358 m_pages.pagesFree.link(pPage);
359 }
360
361 // Allocate a new chunk of memory
362 pBlock = pPage->alloc_block();
363
364 // Handle full pages
365 if (pPage->full()) {
366 // Remove the page from the open list
367 m_pages.pagesFree.unlink(pPage);
368 // Move our page to the full list
369 m_pages.pagesFull.link(pPage);
370 }
371
372 verify();
373 return pBlock;
374 }
375
376 GAIA_CLANG_WARNING_PUSH()
377 // Memory is aligned so we can silence this warning
378 GAIA_CLANG_WARNING_DISABLE("-Wcast-align")
379
381 void free(void* pBlock) {
382 const detail::ArenaLock arenaLock;
383 // Decode the page from the address
384 const auto pageAddr = *(uintptr_t*)((uint8_t*)pBlock - MemoryBlockUsableOffset);
385 GAIA_ASSERT(pageAddr % MemoryBlockAlignment == 0);
386 auto* pPage = (Page*)pageAddr;
387 const bool wasFull = pPage->full();
388
389#if GAIA_ASSERT_ENABLED
390 if (wasFull) {
391 const auto res = m_pages.pagesFull.has(pPage);
392 GAIA_ASSERT(res && "Memory page couldn't be found among full pages");
393 } else {
394 const auto res = m_pages.pagesFree.has(pPage);
395 GAIA_ASSERT(res && "Memory page couldn't be found among free pages");
396 }
397#endif
398
399 // Free the chunk
400 pPage->free_block(pBlock);
401
402 // Update lists
403 if (wasFull) {
404 // Our page is no longer full
405 m_pages.pagesFull.unlink(pPage);
406 // Move our page to the open list
407 m_pages.pagesFree.link(pPage);
408 }
409
410 verify();
411
412 // Special handling for the allocator signaled to destroy itself
413 if (m_isDone) {
414 // Remove the page right away
415 if (pPage->empty()) {
416 GAIA_ASSERT(!m_pages.pagesFree.empty());
417 m_pages.pagesFree.unlink(pPage);
418 }
419
420 try_delete_this();
421 }
422 }
423
424 GAIA_CLANG_WARNING_POP()
425
426
427 MemoryPageStats stats() const {
428 MemoryPageStats stats{};
429
430 stats.num_pages = (uint32_t)m_pages.pagesFree.size() + (uint32_t)m_pages.pagesFull.size();
431 stats.num_pages_free = (uint32_t)m_pages.pagesFree.size();
432 stats.mem_total = stats.num_pages * (size_t)Page::MemoryBlockBytes * Page::NBlocks;
433 stats.mem_used = m_pages.pagesFull.size() * (size_t)Page::MemoryBlockBytes * Page::NBlocks;
434 for (const auto& page: m_pages.pagesFree)
435 stats.mem_used += page.used_blocks_cnt() * (size_t)Page::MemoryBlockBytes;
436
437 return stats;
438 }
439
441 void flush() {
442 const detail::ArenaLock arenaLock;
443 for (auto it = m_pages.pagesFree.begin(); it != m_pages.pagesFree.end();) {
444 auto* pPage = &(*it);
445 ++it;
446
447 // Skip non-empty pages
448 if (!pPage->empty())
449 continue;
450
451 m_pages.pagesFree.unlink(pPage);
452 free_page(pPage);
453 }
454
455 verify();
456 }
457
459 void diag() const {
460 auto memStats = stats();
461 GAIA_LOG_N("PagedAllocator %p stats", (void*)this);
462 GAIA_LOG_N(" Allocated: %" PRIu64 " B", memStats.mem_total);
463 GAIA_LOG_N(" Used: %" PRIu64 " B", memStats.mem_total - memStats.mem_used);
464 GAIA_LOG_N(" Overhead: %" PRIu64 " B", memStats.mem_used);
465 GAIA_LOG_N(
466 " Utilization: %.1f%%",
467 memStats.mem_total != 0 ? 100.0 * ((double)memStats.mem_used / (double)memStats.mem_total) : 0.0);
468 GAIA_LOG_N(" Pages: %u", memStats.num_pages);
469 GAIA_LOG_N(" Free pages: %u", memStats.num_pages_free);
470 }
471
472 void verify() const {
473#if GAIA_ASSERT_ENABLED
474 for (const auto& page: m_pages.pagesFree) {
475 GAIA_ASSERT(page.get_fwd_llist_link().linked());
476 GAIA_ASSERT(!page.full());
477 page.verify();
478 }
479
480 for (const auto& page: m_pages.pagesFull) {
481 GAIA_ASSERT(page.get_fwd_llist_link().linked());
482 GAIA_ASSERT(page.full());
483 page.verify();
484 }
485#endif
486 }
487
488 private:
489 static Page* alloc_page() {
490 const uint32_t size = Page::NBlocks * Page::MemoryBlockBytes;
491 auto* pPageData = mem::AllocHelper::alloc_alig<uint8_t>(&s_strPageData[0], MemoryBlockAlignment, size);
492 auto* pMemoryPage = mem::AllocHelper::alloc<Page>(&s_strMemPage[0]);
493 return new (pMemoryPage) Page(pPageData);
494 }
495
496 static void free_page(Page* pMemoryPage) {
497 GAIA_ASSERT(pMemoryPage != nullptr);
498
499 mem::AllocHelper::free_alig(&s_strPageData[0], pMemoryPage->m_data);
500 pMemoryPage->~MemoryPage();
501 mem::AllocHelper::free(&s_strMemPage[0], pMemoryPage);
502 }
503
504 void done() {
505 m_isDone = true;
506 }
507
508 void try_delete_this() {
509 // When there is nothing left, delete the allocator
510 if (m_pages.empty())
511 delete this;
512 }
513 };
514
515 } // namespace detail
517 } // namespace mem
518} // namespace gaia
Array of elements of type.
Definition darray_ext_impl.h:31
GAIA_NODISCARD pointer data() noexcept
Returns a pointer to the element storage.
Definition darray_ext_impl.h:236
GAIA_NODISCARD bool empty() const noexcept
Checks whether the container has no elements.
Definition darray_ext_impl.h:567
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:255
cnt::fwd_llist< MemoryPage< T, RequestedBlockSize > > pagesFull
List of full pages.
Definition paged_allocator.h:259
GAIA_NODISCARD bool empty() const
Reports whether the container has no pages.
Definition paged_allocator.h:263
cnt::fwd_llist< MemoryPage< T, RequestedBlockSize > > pagesFree
List of available pages.
Definition paged_allocator.h:257
Common header for allocator pages.
Definition paged_allocator.h:30
void * m_data
Pointer to data managed by page.
Definition paged_allocator.h:32
MemoryPageHeader(void *ptr)
Creates a page header for a backing allocation.
Definition paged_allocator.h:36
Aggregate statistics for a paged allocator.
Definition paged_allocator.h:269
uint32_t num_pages_free
Number of free pages.
Definition paged_allocator.h:277
uint64_t mem_used
Memory actively used.
Definition paged_allocator.h:273
uint64_t mem_total
Total allocated memory.
Definition paged_allocator.h:271
uint32_t num_pages
Number of allocated pages.
Definition paged_allocator.h:275
Fixed-capacity page of equal-sized blocks.
Definition paged_allocator.h:43
uint8_t read_block_idx(uint32_t blockIdx) const
Reads one link from the packed recycled-block list.
Definition paged_allocator.h:116
static constexpr uint16_t NBlocks
Maximum number of blocks in a page.
Definition paged_allocator.h:60
void free_block(void *pBlock)
Release the block allocated by this page.
Definition paged_allocator.h:161
void write_block_idx(uint32_t blockIdx, uint32_t value)
Writes one link in the packed recycled-block list.
Definition paged_allocator.h:104
BlockArray m_blocks
Implicit list of blocks.
Definition paged_allocator.h:80
static constexpr uint32_t calculate_block_size()
Selects and aligns the configured block size, including its header.
Definition paged_allocator.h:52
uint32_t m_nextFreeBlock
Index of the next block to recycle.
Definition paged_allocator.h:87
void verify() const
Verifies page invariants in assertion-enabled builds.
Definition paged_allocator.h:215
uint32_t m_freeBlocks
Number of blocks to recycle.
Definition paged_allocator.h:89
GAIA_NODISCARD bool empty() const
Reports whether no page blocks are allocated.
Definition paged_allocator.h:210
GAIA_NODISCARD bool full() const
Reports whether all page blocks are allocated.
Definition paged_allocator.h:204
uint32_t m_usedBlocks
Number of used blocks out of NBlocks.
Definition paged_allocator.h:85
static constexpr uint32_t BlockArrayBytes
Bytes occupied by the packed block-index array.
Definition paged_allocator.h:70
static constexpr uint32_t MemoryBlockBytes
Size of one block in bytes.
Definition paged_allocator.h:58
MemoryPage(void *ptr)
Free bits to use in the future.
Definition paged_allocator.h:95
uint32_t m_blockCnt
Number of blocks in the block array.
Definition paged_allocator.h:83
GAIA_NODISCARD uint32_t used_blocks_cnt() const
Returns the number of live blocks.
Definition paged_allocator.h:198
static constexpr uint32_t InvalidBlockId
Sentinel terminating the recycled-block list.
Definition paged_allocator.h:64
static constexpr uint16_t NBlocks_Bits
Bits required to encode a block index.
Definition paged_allocator.h:62
GAIA_NODISCARD void * alloc_block()
Allocate a new block for this page.
Definition paged_allocator.h:126
static constexpr uint32_t next_multiple_of_alignment(uint32_t num)
Rounds a size up to MemoryBlockAlignment.
Definition paged_allocator.h:47