|
| 1 | +#include "../ds/address.h" |
| 2 | +#include "../ds/flaglock.h" |
| 3 | +#include "../pal/pal.h" |
| 4 | + |
| 5 | +#include <array> |
| 6 | +namespace snmalloc |
| 7 | +{ |
| 8 | + /** |
| 9 | + * Implements a power of two allocator, where all blocks are aligned to the |
| 10 | + * same power of two as their size. This is what snmalloc uses to get |
| 11 | + * alignment of very large sizeclasses. |
| 12 | + * |
| 13 | + * It cannot unreserve memory, so this does not require the |
| 14 | + * usual complexity of a buddy allocator. |
| 15 | + */ |
| 16 | + template<typename Pal> |
| 17 | + class AddressSpaceManager : public Pal |
| 18 | + { |
| 19 | + /** |
| 20 | + * Stores the blocks of address space |
| 21 | + * |
| 22 | + * The first level of array indexes based on power of two size. |
| 23 | + * |
| 24 | + * The first entry ranges[n][0] is just a pointer to an address range |
| 25 | + * of size 2^n. |
| 26 | + * |
| 27 | + * The second entry ranges[n][1] is a pointer to a linked list of blocks |
| 28 | + * of this size. The final block in the list is not committed, so we commit |
| 29 | + * on pop for this corner case. |
| 30 | + * |
| 31 | + * Invariants |
| 32 | + * ranges[n][1] != nullptr => ranges[n][0] != nullptr |
| 33 | + * |
| 34 | + * bits::BITS is used for simplicity, we do not use below the pointer size, |
| 35 | + * and large entries will be unlikely to be supported by the platform. |
| 36 | + */ |
| 37 | + std::array<std::array<void*, 2>, bits::BITS> ranges = {}; |
| 38 | + |
| 39 | + /** |
| 40 | + * This is infrequently used code, a spin lock simplifies the code |
| 41 | + * considerably, and should never be on the fast path. |
| 42 | + */ |
| 43 | + std::atomic_flag spin_lock = ATOMIC_FLAG_INIT; |
| 44 | + |
| 45 | + /** |
| 46 | + * Checks a block satisfies its invariant. |
| 47 | + */ |
| 48 | + inline void check_block(void* base, size_t align_bits) |
| 49 | + { |
| 50 | + SNMALLOC_ASSERT( |
| 51 | + base == pointer_align_up(base, bits::one_at_bit(align_bits))); |
| 52 | + // All blocks need to be bigger than a pointer. |
| 53 | + SNMALLOC_ASSERT(bits::one_at_bit(align_bits) >= sizeof(void*)); |
| 54 | + UNUSED(base); |
| 55 | + UNUSED(align_bits); |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * Adds a block to `ranges`. |
| 60 | + */ |
| 61 | + void add_block(size_t align_bits, void* base) |
| 62 | + { |
| 63 | + check_block(base, align_bits); |
| 64 | + SNMALLOC_ASSERT(align_bits < 64); |
| 65 | + if (ranges[align_bits][0] == nullptr) |
| 66 | + { |
| 67 | + // Prefer first slot if available. |
| 68 | + ranges[align_bits][0] = base; |
| 69 | + return; |
| 70 | + } |
| 71 | + |
| 72 | + if (ranges[align_bits][1] != nullptr) |
| 73 | + { |
| 74 | + // Add to linked list. |
| 75 | + commit_block(base, sizeof(void*)); |
| 76 | + *reinterpret_cast<void**>(base) = ranges[align_bits][1]; |
| 77 | + check_block(ranges[align_bits][1], align_bits); |
| 78 | + } |
| 79 | + |
| 80 | + // Update head of list |
| 81 | + ranges[align_bits][1] = base; |
| 82 | + check_block(ranges[align_bits][1], align_bits); |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Find a block of the correct size. May split larger blocks |
| 87 | + * to satisfy this request. |
| 88 | + */ |
| 89 | + void* remove_block(size_t align_bits) |
| 90 | + { |
| 91 | + auto first = ranges[align_bits][0]; |
| 92 | + if (first == nullptr) |
| 93 | + { |
| 94 | + if (align_bits == (bits::BITS - 1)) |
| 95 | + { |
| 96 | + // Out of memory |
| 97 | + return nullptr; |
| 98 | + } |
| 99 | + |
| 100 | + // Look for larger block and split up recursively |
| 101 | + void* bigger = remove_block(align_bits + 1); |
| 102 | + if (bigger != nullptr) |
| 103 | + { |
| 104 | + void* left_over = |
| 105 | + pointer_offset(bigger, bits::one_at_bit(align_bits)); |
| 106 | + ranges[align_bits][0] = left_over; |
| 107 | + check_block(left_over, align_bits); |
| 108 | + } |
| 109 | + check_block(bigger, align_bits + 1); |
| 110 | + return bigger; |
| 111 | + } |
| 112 | + |
| 113 | + auto second = ranges[align_bits][1]; |
| 114 | + if (second != nullptr) |
| 115 | + { |
| 116 | + commit_block(second, sizeof(void*)); |
| 117 | + auto next = *reinterpret_cast<void**>(second); |
| 118 | + ranges[align_bits][1] = next; |
| 119 | + // Zero memory. Client assumes memory contains only zeros. |
| 120 | + *reinterpret_cast<void**>(second) = nullptr; |
| 121 | + check_block(second, align_bits); |
| 122 | + check_block(next, align_bits); |
| 123 | + return second; |
| 124 | + } |
| 125 | + |
| 126 | + check_block(first, align_bits); |
| 127 | + ranges[align_bits][0] = nullptr; |
| 128 | + return first; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Add a range of memory to the address space. |
| 133 | + * Divides blocks into power of two sizes with natural alignment |
| 134 | + */ |
| 135 | + void add_range(void* base, size_t length) |
| 136 | + { |
| 137 | + // Find the minimum set of maximally aligned blocks in this range. |
| 138 | + // Each block's alignment and size are equal. |
| 139 | + while (length >= sizeof(void*)) |
| 140 | + { |
| 141 | + size_t base_align_bits = bits::ctz(address_cast(base)); |
| 142 | + size_t length_align_bits = (bits::BITS - 1) - bits::clz(length); |
| 143 | + size_t align_bits = bits::min(base_align_bits, length_align_bits); |
| 144 | + size_t align = bits::one_at_bit(align_bits); |
| 145 | + |
| 146 | + check_block(base, align_bits); |
| 147 | + add_block(align_bits, base); |
| 148 | + |
| 149 | + base = pointer_offset(base, align); |
| 150 | + length -= align; |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * Commit a block of memory |
| 156 | + */ |
| 157 | + void commit_block(void* base, size_t size) |
| 158 | + { |
| 159 | + // Rounding required for sub-page allocations. |
| 160 | + auto page_start = pointer_align_down<OS_PAGE_SIZE, char>(base); |
| 161 | + auto page_end = |
| 162 | + pointer_align_up<OS_PAGE_SIZE, char>(pointer_offset(base, size)); |
| 163 | + Pal::template notify_using<NoZero>( |
| 164 | + page_start, static_cast<size_t>(page_end - page_start)); |
| 165 | + } |
| 166 | + |
| 167 | + public: |
| 168 | + /** |
| 169 | + * Returns a pointer to a block of memory of the supplied size. |
| 170 | + * The block will be committed, if specified by the template parameter. |
| 171 | + * The returned block is guaranteed to be aligened to the size. |
| 172 | + * |
| 173 | + * Only request 2^n sizes, and not less than a pointer. |
| 174 | + */ |
| 175 | + template<bool committed> |
| 176 | + void* reserve(size_t size) |
| 177 | + { |
| 178 | + SNMALLOC_ASSERT(bits::next_pow2(size) == size); |
| 179 | + SNMALLOC_ASSERT(size >= sizeof(void*)); |
| 180 | + |
| 181 | + if constexpr (pal_supports<AlignedAllocation, Pal>) |
| 182 | + { |
| 183 | + if (size >= Pal::minimum_alloc_size) |
| 184 | + return static_cast<Pal*>(this)->template reserve_aligned<committed>( |
| 185 | + size); |
| 186 | + } |
| 187 | + |
| 188 | + void* res; |
| 189 | + { |
| 190 | + FlagLock lock(spin_lock); |
| 191 | + res = remove_block(bits::next_pow2_bits(size)); |
| 192 | + if (res == nullptr) |
| 193 | + { |
| 194 | + // Allocation failed ask OS for more memory |
| 195 | + void* block; |
| 196 | + size_t block_size; |
| 197 | + if constexpr (pal_supports<AlignedAllocation, Pal>) |
| 198 | + { |
| 199 | + block_size = Pal::minimum_alloc_size; |
| 200 | + block = static_cast<Pal*>(this)->template reserve_aligned<false>( |
| 201 | + block_size); |
| 202 | + } |
| 203 | + else |
| 204 | + { |
| 205 | + // Need at least 2 times the space to guarantee alignment. |
| 206 | + // Hold lock here as a race could cause additional requests to |
| 207 | + // the Pal, and this could lead to suprious OOM. This is |
| 208 | + // particularly bad if the Pal gives all the memory on first call. |
| 209 | + auto block_and_size = |
| 210 | + static_cast<Pal*>(this)->reserve_at_least(size * 2); |
| 211 | + block = block_and_size.first; |
| 212 | + block_size = block_and_size.second; |
| 213 | + |
| 214 | + // Ensure block is pointer aligned. |
| 215 | + if ( |
| 216 | + pointer_align_up(block, sizeof(void*)) != block || |
| 217 | + bits::align_up(block_size, sizeof(void*)) > block_size) |
| 218 | + { |
| 219 | + auto diff = |
| 220 | + pointer_diff(block, pointer_align_up(block, sizeof(void*))); |
| 221 | + block_size = block_size - diff; |
| 222 | + block_size = bits::align_down(block_size, sizeof(void*)); |
| 223 | + } |
| 224 | + } |
| 225 | + if (block == nullptr) |
| 226 | + { |
| 227 | + return nullptr; |
| 228 | + } |
| 229 | + add_range(block, block_size); |
| 230 | + |
| 231 | + // still holding lock so guaranteed to succeed. |
| 232 | + res = remove_block(bits::next_pow2_bits(size)); |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + // Don't need lock while committing pages. |
| 237 | + if constexpr (committed) |
| 238 | + commit_block(res, size); |
| 239 | + |
| 240 | + return res; |
| 241 | + } |
| 242 | + }; |
| 243 | +} // namespace snmalloc |
0 commit comments