358 lines
12 KiB
C++
358 lines
12 KiB
C++
#include "../ds/address.h"
|
|
#include "../ds/flaglock.h"
|
|
#include "../pal/pal.h"
|
|
#include "arenamap.h"
|
|
|
|
#include <array>
|
|
namespace snmalloc
|
|
{
|
|
/**
|
|
* Implements a power of two allocator, where all blocks are aligned to the
|
|
* same power of two as their size. This is what snmalloc uses to get
|
|
* alignment of very large sizeclasses.
|
|
*
|
|
* It cannot unreserve memory, so this does not require the
|
|
* usual complexity of a buddy allocator.
|
|
*/
|
|
template<SNMALLOC_CONCEPT(ConceptPAL) PAL, typename ArenaMap>
|
|
class AddressSpaceManager
|
|
{
|
|
/**
|
|
* Stores the blocks of address space
|
|
*
|
|
* The first level of array indexes based on power of two size.
|
|
*
|
|
* The first entry ranges[n][0] is just a pointer to an address range
|
|
* of size 2^n.
|
|
*
|
|
* The second entry ranges[n][1] is a pointer to a linked list of blocks
|
|
* of this size. The final block in the list is not committed, so we commit
|
|
* on pop for this corner case.
|
|
*
|
|
* Invariants
|
|
* ranges[n][1] != nullptr => ranges[n][0] != nullptr
|
|
*
|
|
* bits::BITS is used for simplicity, we do not use below the pointer size,
|
|
* and large entries will be unlikely to be supported by the platform.
|
|
*/
|
|
std::array<std::array<CapPtr<void, CBChunk>, 2>, bits::BITS> ranges = {};
|
|
|
|
/**
|
|
* This is infrequently used code, a spin lock simplifies the code
|
|
* considerably, and should never be on the fast path.
|
|
*/
|
|
std::atomic_flag spin_lock = ATOMIC_FLAG_INIT;
|
|
|
|
/**
|
|
* Checks a block satisfies its invariant.
|
|
*/
|
|
inline void check_block(CapPtr<void, CBChunk> base, size_t align_bits)
|
|
{
|
|
SNMALLOC_ASSERT(
|
|
base == pointer_align_up(base, bits::one_at_bit(align_bits)));
|
|
// All blocks need to be bigger than a pointer.
|
|
SNMALLOC_ASSERT(bits::one_at_bit(align_bits) >= sizeof(void*));
|
|
UNUSED(base);
|
|
UNUSED(align_bits);
|
|
}
|
|
|
|
/**
|
|
* Adds a block to `ranges`.
|
|
*/
|
|
void add_block(size_t align_bits, CapPtr<void, CBChunk> base)
|
|
{
|
|
check_block(base, align_bits);
|
|
SNMALLOC_ASSERT(align_bits < 64);
|
|
if (ranges[align_bits][0] == nullptr)
|
|
{
|
|
// Prefer first slot if available.
|
|
ranges[align_bits][0] = base;
|
|
return;
|
|
}
|
|
|
|
if (ranges[align_bits][1] != nullptr)
|
|
{
|
|
// Add to linked list.
|
|
commit_block(base, sizeof(void*));
|
|
*(base.template as_static<CapPtr<void, CBChunk>>().unsafe_capptr) =
|
|
ranges[align_bits][1];
|
|
check_block(ranges[align_bits][1], align_bits);
|
|
}
|
|
|
|
// Update head of list
|
|
ranges[align_bits][1] = base;
|
|
check_block(ranges[align_bits][1], align_bits);
|
|
}
|
|
|
|
/**
|
|
* Find a block of the correct size. May split larger blocks
|
|
* to satisfy this request.
|
|
*/
|
|
CapPtr<void, CBChunk> remove_block(size_t align_bits)
|
|
{
|
|
CapPtr<void, CBChunk> first = ranges[align_bits][0];
|
|
if (first == nullptr)
|
|
{
|
|
if (align_bits == (bits::BITS - 1))
|
|
{
|
|
// Out of memory
|
|
return nullptr;
|
|
}
|
|
|
|
// Look for larger block and split up recursively
|
|
CapPtr<void, CBChunk> bigger = remove_block(align_bits + 1);
|
|
if (bigger != nullptr)
|
|
{
|
|
size_t left_over_size = bits::one_at_bit(align_bits);
|
|
auto left_over = pointer_offset(bigger, left_over_size);
|
|
ranges[align_bits][0] =
|
|
Aal::capptr_bound<void, CBChunk>(left_over, left_over_size);
|
|
check_block(left_over, align_bits);
|
|
}
|
|
check_block(bigger, align_bits + 1);
|
|
return bigger;
|
|
}
|
|
|
|
CapPtr<void, CBChunk> second = ranges[align_bits][1];
|
|
if (second != nullptr)
|
|
{
|
|
commit_block(second, sizeof(void*));
|
|
auto psecond =
|
|
second.template as_static<CapPtr<void, CBChunk>>().unsafe_capptr;
|
|
auto next = *psecond;
|
|
ranges[align_bits][1] = next;
|
|
// Zero memory. Client assumes memory contains only zeros.
|
|
*psecond = nullptr;
|
|
check_block(second, align_bits);
|
|
check_block(next, align_bits);
|
|
return second;
|
|
}
|
|
|
|
check_block(first, align_bits);
|
|
ranges[align_bits][0] = nullptr;
|
|
return first;
|
|
}
|
|
|
|
/**
|
|
* Add a range of memory to the address space.
|
|
* Divides blocks into power of two sizes with natural alignment
|
|
*/
|
|
void add_range(CapPtr<void, CBChunk> base, size_t length)
|
|
{
|
|
// Find the minimum set of maximally aligned blocks in this range.
|
|
// Each block's alignment and size are equal.
|
|
while (length >= sizeof(void*))
|
|
{
|
|
size_t base_align_bits = bits::ctz(address_cast(base));
|
|
size_t length_align_bits = (bits::BITS - 1) - bits::clz(length);
|
|
size_t align_bits = bits::min(base_align_bits, length_align_bits);
|
|
size_t align = bits::one_at_bit(align_bits);
|
|
|
|
check_block(base, align_bits);
|
|
add_block(align_bits, base);
|
|
|
|
base = pointer_offset(base, align);
|
|
length -= align;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commit a block of memory
|
|
*/
|
|
void commit_block(CapPtr<void, CBChunk> base, size_t size)
|
|
{
|
|
// Rounding required for sub-page allocations.
|
|
auto page_start = pointer_align_down<OS_PAGE_SIZE, char>(base);
|
|
auto page_end =
|
|
pointer_align_up<OS_PAGE_SIZE, char>(pointer_offset(base, size));
|
|
size_t using_size = pointer_diff(page_start, page_end);
|
|
PAL::template notify_using<NoZero>(page_start.unsafe_capptr, using_size);
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* Returns a pointer to a block of memory of the supplied size.
|
|
* The block will be committed, if specified by the template parameter.
|
|
* The returned block is guaranteed to be aligened to the size.
|
|
*
|
|
* Only request 2^n sizes, and not less than a pointer.
|
|
*
|
|
* On StrictProvenance architectures, any underlying allocations made as
|
|
* part of satisfying the request will be registered with the provided
|
|
* arena_map for use in subsequent amplification.
|
|
*/
|
|
template<bool committed>
|
|
CapPtr<void, CBChunk> reserve(size_t size, ArenaMap& arena_map)
|
|
{
|
|
SNMALLOC_ASSERT(bits::is_pow2(size));
|
|
SNMALLOC_ASSERT(size >= sizeof(void*));
|
|
|
|
/*
|
|
* For sufficiently large allocations with platforms that support aligned
|
|
* allocations and architectures that don't require StrictProvenance,
|
|
* try asking the platform first.
|
|
*/
|
|
if constexpr (
|
|
pal_supports<AlignedAllocation, PAL> && !aal_supports<StrictProvenance>)
|
|
{
|
|
if (size >= PAL::minimum_alloc_size)
|
|
return CapPtr<void, CBChunk>(
|
|
PAL::template reserve_aligned<committed>(size));
|
|
}
|
|
|
|
CapPtr<void, CBChunk> res;
|
|
{
|
|
FlagLock lock(spin_lock);
|
|
res = remove_block(bits::next_pow2_bits(size));
|
|
if (res == nullptr)
|
|
{
|
|
// Allocation failed ask OS for more memory
|
|
CapPtr<void, CBChunk> block = nullptr;
|
|
size_t block_size = 0;
|
|
if constexpr (pal_supports<AlignedAllocation, PAL>)
|
|
{
|
|
/*
|
|
* aal_supports<StrictProvenance> ends up here, too, and we ensure
|
|
* that we always allocate whole ArenaMap granules.
|
|
*/
|
|
if constexpr (aal_supports<StrictProvenance>)
|
|
{
|
|
static_assert(
|
|
!aal_supports<StrictProvenance> ||
|
|
(ArenaMap::alloc_size >= PAL::minimum_alloc_size),
|
|
"Provenance root granule must be at least PAL's "
|
|
"minimum_alloc_size");
|
|
block_size = bits::align_up(size, ArenaMap::alloc_size);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* We will have handled the case where size >= minimum_alloc_size
|
|
* above, so we are left to handle only small things here.
|
|
*/
|
|
block_size = PAL::minimum_alloc_size;
|
|
}
|
|
|
|
void* block_raw = PAL::template reserve_aligned<false>(block_size);
|
|
|
|
// It's a bit of a lie to convert without applying bounds, but the
|
|
// platform will have bounded block for us and it's better that the
|
|
// rest of our internals expect CBChunk bounds.
|
|
block = CapPtr<void, CBChunk>(block_raw);
|
|
|
|
if constexpr (aal_supports<StrictProvenance>)
|
|
{
|
|
auto root_block = CapPtr<void, CBArena>(block_raw);
|
|
auto root_size = block_size;
|
|
do
|
|
{
|
|
arena_map.register_root(root_block);
|
|
root_block = pointer_offset(root_block, ArenaMap::alloc_size);
|
|
root_size -= ArenaMap::alloc_size;
|
|
} while (root_size > 0);
|
|
}
|
|
}
|
|
else if constexpr (!pal_supports<NoAllocation, PAL>)
|
|
{
|
|
// Need at least 2 times the space to guarantee alignment.
|
|
// Hold lock here as a race could cause additional requests to
|
|
// the PAL, and this could lead to suprious OOM. This is
|
|
// particularly bad if the PAL gives all the memory on first call.
|
|
auto block_and_size = PAL::reserve_at_least(size * 2);
|
|
block = CapPtr<void, CBChunk>(block_and_size.first);
|
|
block_size = block_and_size.second;
|
|
|
|
// Ensure block is pointer aligned.
|
|
if (
|
|
pointer_align_up(block, sizeof(void*)) != block ||
|
|
bits::align_up(block_size, sizeof(void*)) > block_size)
|
|
{
|
|
auto diff =
|
|
pointer_diff(block, pointer_align_up(block, sizeof(void*)));
|
|
block_size = block_size - diff;
|
|
block_size = bits::align_down(block_size, sizeof(void*));
|
|
}
|
|
}
|
|
if (block == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
add_range(block, block_size);
|
|
|
|
// still holding lock so guaranteed to succeed.
|
|
res = remove_block(bits::next_pow2_bits(size));
|
|
}
|
|
}
|
|
|
|
// Don't need lock while committing pages.
|
|
if constexpr (committed)
|
|
commit_block(res, size);
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Aligns block to next power of 2 above size, and unused space at the end
|
|
* of the block is retained by the address space manager.
|
|
*
|
|
* This is useful for allowing the space required for alignment to be
|
|
* used, by smaller objects.
|
|
*/
|
|
template<bool committed>
|
|
CapPtr<void, CBChunk>
|
|
reserve_with_left_over(size_t size, ArenaMap& arena_map)
|
|
{
|
|
SNMALLOC_ASSERT(size >= sizeof(void*));
|
|
|
|
size = bits::align_up(size, sizeof(void*));
|
|
|
|
size_t rsize = bits::next_pow2(size);
|
|
|
|
auto res = reserve<false>(rsize, arena_map);
|
|
|
|
if (res != nullptr)
|
|
{
|
|
if (rsize > size)
|
|
{
|
|
FlagLock lock(spin_lock);
|
|
add_range(pointer_offset(res, size), rsize - size);
|
|
}
|
|
|
|
if constexpr (committed)
|
|
commit_block(res, size);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Default constructor. An address-space manager constructed in this way
|
|
* does not own any memory at the start and will request any that it needs
|
|
* from the PAL.
|
|
*/
|
|
AddressSpaceManager() = default;
|
|
|
|
/**
|
|
* Constructor that pre-initialises the address-space manager with a region
|
|
* of memory.
|
|
*/
|
|
AddressSpaceManager(CapPtr<void, CBChunk> base, size_t length)
|
|
{
|
|
add_range(base, length);
|
|
}
|
|
|
|
/**
|
|
* Move assignment operator. This should only be used during initialisation
|
|
* of the system. There should be no concurrency.
|
|
*/
|
|
AddressSpaceManager& operator=(AddressSpaceManager&& other) noexcept
|
|
{
|
|
// Lock address space manager. This will prevent it being used by
|
|
// mistake. Fails with deadlock with any subsequent caller.
|
|
if (other.spin_lock.test_and_set())
|
|
abort();
|
|
ranges = other.ranges;
|
|
return *this;
|
|
}
|
|
};
|
|
} // namespace snmalloc
|