Skip to content

Commit e16f2af

Browse files
authored
Add AddressSpaceManager (#214)
This change brings in a new approach to managing address space. It wraps the Pal with a power of two reservation system, that guarantees all returned blocks are naturally aligned to their size. It either lets the Pal perform aligned requests, or over allocates and splits into power of two blocks.
1 parent e393ac8 commit e16f2af

File tree

9 files changed

+352
-281
lines changed

9 files changed

+352
-281
lines changed

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,24 @@ pages, rather than zeroing them synchronously in this call
194194

195195
```c++
196196
template<bool committed>
197-
void* reserve(size_t size, size_t align);
198-
template<bool committed>
199-
void* reserve(size_t size) noexcept;
197+
void* reserve_aligned(size_t size) noexcept;
198+
std::pair<void*, size_t> reserve_at_least(size_t size) noexcept;
200199
```
201200
Only one of these needs to be implemented, depending on whether the underlying
202201
system can provide strongly aligned memory regions.
203-
If the system guarantees only page alignment, implement the second and snmalloc
204-
will over-allocate and then trim the requested region.
202+
If the system guarantees only page alignment, implement the second. The Pal is
203+
free to overallocate based on the platforms desire and snmalloc
204+
will find suitably aligned blocks inside the region. `reserve_at_least` should
205+
not commit memory as snmalloc will commit the range of memory it requires of what
206+
is returned.
207+
205208
If the system provides strong alignment, implement the first to return memory
206-
at the desired alignment.
209+
at the desired alignment. If providing the first, then the `Pal` should also
210+
specify the minimum size block it can provide:
211+
```
212+
static constexpr size_t minimum_alloc_size = ...;
213+
```
214+
207215
208216
Finally, you need to define a field to indicate the features that your PAL supports:
209217
```c++

src/mem/address_space.h

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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

Comments
 (0)