Ark Server API (ASA) - Wiki
Loading...
Searching...
No Matches
PooledVirtualMemoryAllocator.h
Go to the documentation of this file.
1// Copyright Epic Games, Inc. All Rights Reserved.
2
3#pragma once
4
5#include "CoreTypes.h"
6#include "HAL/CriticalSection.h"
7#include "HAL/Allocators/CachedOSPageAllocator.h"
8
9/**
10 * This class pools OS allocations made from FMallocBinned2.
11 *
12 * The class fulfills FMallocBinned2 requirements of returning a 64KB-aligned address
13 * and it also avoids fragmenting the memory into too many small VMAs (virtual memory areas).
14 *
15 * The logic is like follows:
16 *
17 * There are N buckets that represent allocation sizes from 64KB to N*64 KB. Each bucket is a linked list
18 * of pools that hold varied number of same-sized allocations (called blocks). Each bucket starts empty (linked list is empty).
19 *
20 * Whenever an allocation request arrives, it is first bucketed based on its size (if it is larger than the largest bucket,
21 * it is passed through to a caching OS allocator). Then, the linked list of that bucket is walked, and
22 * the allocation is fulfilled by the first pool that has empty "blocks". If there are no such pool, a new pool is created
23 * (with number of blocks being possibly larger than in the existing list, if any), this pool becomes the new head of the
24 * list and the allocation happens there.
25 *
26 * Whenever a free request arrives, it is again first bucketed based on the size (which must be passed in and *must* match
27 * allocation size). If it is larger than the largest bucket, it is passed through to a platform function BinnedFreeToOS().
28 * Otherwise, the appropriate bucket's list of pools is walked to find the pool where the allocation belongs, and the
29 * appropriate block becomes free (a platform-specific function is called to evict the memory range from the RAM).
30 * If this was the last used block in the pool, the whole pool is destroyed and the list shrinks by one.
31 *
32 * So, to visualize an example state:
33 *
34 * 64KB bucket: Head -> [ A pool of 200 64KB "blocks", of which 50 are empty ] -> [ A pool of 100 64KB blocks, of which 30 are empty ] -> null
35 * 128KB bucket: Head -> [ A pool of 60 128KB "blocks", of which 25 are empty ] -> null
36 * 192KB bucket: Head -> null
37 * 256KB bucket: Head -> [ A pool of 40 256KB "blocks", of which 10 are empty ] -> [ A pool of 20 256KB blocks, of which 10 are emtpy ] -> [ A pool of 4 256 KB blocks of which 0 are empty ] -> null
38 * ...
39 * 4MB bucket: Head -> [ A pool of 2 4MB "blocks", of which 1 is empty ] -> null
40 *
41 * Each pool uses one distinct VMA on Linux (or one distinct VirtualAlloc on Windows).
42 *
43 * The class also maintains an idea of what current size of each pool (per bucket) should be.
44 * Each time we add a new pool to a particular bucket, this size can grow (exponentially or otherwise).
45 * Each time we delete a pool from a particular bucket, this size can shrink.
46 * The logic that handles that is in DecideOnTheNextPoolSize().
47 *
48 * Right now the class is less multithread friendly than it could be. There are per-bucket locks
49 * so allocations of different sizes would not block each other if happening on different threads.
50 * It is possible to make allocations within one bucket lock-free as well (e.g. with hazard pointers), but
51 * since FMallocBinned2 itself needs a lock to maintain its own structures when making a call here,
52 * the point is moot.
53 *
54 * This class is somewhat similar to FCachedOSPageAllocator. However, unlike FCOSPA, this is not a cache
55 * and we cannot "trim" anything here. Also, it does not make sense to put the global cap on the pooled memory
56 * since BinnedAllocFromOS() can support only a limited number of allocations on some platforms.
57 *
58 * CachedOSPageAllocator sits "below" this and is used for allocs larger than the largest bucketed.
59 */
61{
63
64 void* Allocate(SIZE_T Size, uint32 AllocationHint = 0, FCriticalSection* Mutex = nullptr);
65 void Free(void* Ptr, SIZE_T Size, FCriticalSection* Mutex = nullptr, bool ThreadIsTimeCritical = false);
66 void FreeAll(FCriticalSection* Mutex = nullptr);
67
68 /** A structure that describes a pool of a particular size */
70 {
71 /** Next in the list */
73
74 /** Total size to be deallocated, which includes both pool memory and all the descriptor/bookkeeping memory */
76 };
77
78 /** Returns free memory in the pools */
80
81 /** Refresh allocator if needed. (does nothing in that implementation) */
82 void Refresh() {}
83
84 /** Update memory stats (does nothing in that implementation) */
85 void UpdateStats() {}
86
87private:
88
89 enum Limits
90 {
93
94 MaxOSAllocCacheSize = 64 * 1024 * 1024,
96 };
97
98 /**
99 * Buckets allocations by its size.
100 *
101 * Allocation size class is an index into a number of tables containing per-class elements.
102 * Index of 0 (smallest possible) represents allocations of size 65536 or less,
103 * index of NumAllocationSizeClasses - 1 (largest possible) represents allocations of size
104 * larger or equal than (NumAllocationSizeClasses - 1 * 65536) and smaller than (NumAllocationSizeClasses * 65536).
105 *
106 * This function should not be passed 0 as Size!
107 */
109 {
110 return static_cast<int32>(Size >> 16) + ((Size & 0xFFFF) ? 1 : 0) - 1;
111 }
112
113 /**
114 * Returns allocation size for a class.
115 */
117 {
118 return (static_cast<SIZE_T>(Class) + 1) * 65536;
119 }
120
121 /**
122 * We only pool allocations that are divisible by 64KB.
123 * Max allocation size is pooled is NumAllocationSizeClasses * 64KB, after which
124 * we just allocate directly from the OS.
125 *
126 * This array keeps the number of pooled allocations for each
127 * allocation size class that we keep bumping up whenever the pool is exhausted.
128 */
130
131 /** Head of the pool descriptor lists */
133
134 /** Per-class locks */
136
137 /** Increases the number of pooled allocations next time we need a pool
138 *
139 * @param bGrowing - if true, we are allocating it, if false, we have just deleted a pool of this size
140 */
141 void DecideOnTheNextPoolSize(int32 SizeClass, bool bGrowing);
142
143 /** Allocates a new pool */
144 FPoolDescriptorBase* CreatePool(SIZE_T AllocationSize, int32 NumPooledAllocations);
145
146 /** Destroys a pool */
148
149 /** Lock for accessing the caching allocator for larger allocs */
151
152 /** Caching allocator for larger allocs */
154};
#define FORCEINLINE
Definition Platform.h:644
FWindowsCriticalSection FCriticalSection
FORCEINLINE int32 GetAllocationSizeClass(SIZE_T Size)
int32 NextPoolSize[Limits::NumAllocationSizeClasses]
FPoolDescriptorBase * ClassesListHeads[Limits::NumAllocationSizeClasses]
TCachedOSPageAllocator< MaxOSAllocsCached, MaxOSAllocCacheSize > OsAllocatorCache
FORCEINLINE SIZE_T CalculateAllocationSizeFromClass(int32 Class)
void DecideOnTheNextPoolSize(int32 SizeClass, bool bGrowing)
void * Allocate(SIZE_T Size, uint32 AllocationHint=0, FCriticalSection *Mutex=nullptr)
void DestroyPool(FPoolDescriptorBase *Pool)
FCriticalSection ClassesLocks[Limits::NumAllocationSizeClasses]
void FreeAll(FCriticalSection *Mutex=nullptr)
FPoolDescriptorBase * CreatePool(SIZE_T AllocationSize, int32 NumPooledAllocations)
void Free(void *Ptr, SIZE_T Size, FCriticalSection *Mutex=nullptr, bool ThreadIsTimeCritical=false)