#pragma once #include "../ds/flaglock.h" #include "../ds/helpers.h" #include "../ds/mpmcstack.h" #include "../pal/pal.h" #include "address_space.h" #include "allocstats.h" #include "baseslab.h" #include "sizeclass.h" #include #include namespace snmalloc { template class MemoryProviderStateMixin; class Largeslab : public Baseslab { // This is the view of a contiguous memory area when it is being kept // in the global size-classed caches of available contiguous memory areas. private: template friend class MPMCStack; template friend class MemoryProviderStateMixin; std::atomic next; public: void init() { kind = Large; } }; /** * A slab that has been decommitted. The first page remains committed and * the only fields that are guaranteed to exist are the kind and next * pointer from the superclass. */ struct Decommittedslab : public Largeslab { /** * Constructor. Expected to be called via placement new into some memory * that was formerly a superslab or large allocation and is now just some * spare address space. */ Decommittedslab() { kind = Decommitted; } }; // This represents the state that the large allcoator needs to add to the // global state of the allocator. This is currently stored in the memory // provider, so we add this in. template class MemoryProviderStateMixin : public PalNotificationObject, public PAL { /** * Simple flag for checking if another instance of lazy-decommit is * running */ std::atomic_flag lazy_decommit_guard = {}; /** * Manages address space for this memory provider. */ AddressSpaceManager address_space = {}; public: /** * Stack of large allocations that have been returned for reuse. */ ModArray> large_stack; /** * Make a new memory provide for this PAL. */ static MemoryProviderStateMixin* make() noexcept { // Temporary stack-based storage to start the allocator in. MemoryProviderStateMixin local{}; // Allocate permanent storage for the allocator usung temporary allocator MemoryProviderStateMixin* allocated = local.alloc_chunk, 1>(); if (allocated == nullptr) error("Failed to initialise system!"); #ifdef GCC_VERSION_EIGHT_PLUS # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wclass-memaccess" #endif // Put temporary allocator we have used, into the permanent storage. // memcpy is safe as this is entirely single threaded: the move // constructors were removed as unsafe to move std::atomic in a // concurrent setting. ::memcpy( &(allocated->address_space), &(local.address_space), sizeof(AddressSpaceManager)); #ifdef GCC_VERSION_EIGHT_PLUS # pragma GCC diagnostic pop #endif // Register this allocator for low-memory call-backs if constexpr (pal_supports) { allocated->PalNotificationObject::pal_notify = &(allocated->process); PAL::register_for_low_memory_callback(allocated); } return allocated; } private: SNMALLOC_SLOW_PATH void lazy_decommit() { // If another thread is try to do lazy decommit, let it continue. If // we try to parallelise this, we'll most likely end up waiting on the // same page table locks. if (!lazy_decommit_guard.test_and_set()) { return; } // When we hit low memory, iterate over size classes and decommit all of // the memory that we can. Start with the small size classes so that we // hit cached superslabs first. // FIXME: We probably shouldn't do this all at once. // FIXME: We currently Decommit all the sizeclasses larger than 0. for (size_t large_class = 0; large_class < NUM_LARGE_CLASSES; large_class++) { if (!PAL::expensive_low_memory_check()) { break; } size_t rsize = bits::one_at_bit(SUPERSLAB_BITS) << large_class; size_t decommit_size = rsize - OS_PAGE_SIZE; // Grab all of the chunks of this size class. auto* slab = large_stack[large_class].pop_all(); while (slab) { // Decommit all except for the first page and then put it back on // the stack. if (slab->get_kind() != Decommitted) { PAL::notify_not_using( pointer_offset(slab, OS_PAGE_SIZE), decommit_size); } // Once we've removed these from the stack, there will be no // concurrent accesses and removal should have established a // happens-before relationship, so it's safe to use relaxed loads // here. auto next = slab->next.load(std::memory_order_relaxed); large_stack[large_class].push(new (slab) Decommittedslab()); slab = next; } } lazy_decommit_guard.clear(); } /*** * Method for callback object to perform lazy decommit. */ static void process(PalNotificationObject* p) { // Unsafe downcast here. Don't want vtable and RTTI. auto self = reinterpret_cast*>(p); self->lazy_decommit(); } public: /** * Primitive allocator for structure that are required before * the allocator can be running. */ template T* alloc_chunk(Args&&... args) { // Cache line align size_t size = bits::align_up(sizeof(T), 64); size = bits::max(size, alignment); void* p = address_space.template reserve(bits::next_pow2(size)); if (p == nullptr) return nullptr; return new (p) T(std::forward(args)...); } template void* reserve(size_t large_class) noexcept { size_t size = bits::one_at_bit(SUPERSLAB_BITS) << large_class; return address_space.template reserve(size); } }; using Stats = AllocStats; enum AllowReserve { NoReserve, YesReserve }; template class LargeAlloc { public: // This will be a zero-size structure if stats are not enabled. Stats stats; MemoryProvider& memory_provider; LargeAlloc(MemoryProvider& mp) : memory_provider(mp) {} template void* alloc(size_t large_class, size_t size) { size_t rsize = bits::one_at_bit(SUPERSLAB_BITS) << large_class; // For superslab size, we always commit the whole range. if (large_class == 0) size = rsize; void* p = memory_provider.large_stack[large_class].pop(); if (p == nullptr) { p = memory_provider.template reserve(large_class); if (p == nullptr) return nullptr; memory_provider.template notify_using(p, rsize); } else { stats.superslab_pop(); // Cross-reference alloc.h's large_dealloc decommitment condition. bool decommitted = ((decommit_strategy == DecommitSuperLazy) && (static_cast(p)->get_kind() == Decommitted)) || (large_class > 0) || (decommit_strategy == DecommitSuper); if (decommitted) { // The first page is already in "use" for the stack element, // this will need zeroing for a YesZero call. if constexpr (zero_mem == YesZero) memory_provider.template zero(p, OS_PAGE_SIZE); // Notify we are using the rest of the allocation. // Passing zero_mem ensures the PAL provides zeroed pages if // required. memory_provider.template notify_using( pointer_offset(p, OS_PAGE_SIZE), rsize - OS_PAGE_SIZE); } else { // This is a superslab that has not been decommitted. if constexpr (zero_mem == YesZero) memory_provider.template zero( p, bits::align_up(size, OS_PAGE_SIZE)); else UNUSED(size); } } SNMALLOC_ASSERT(p == pointer_align_up(p, rsize)); return p; } void dealloc(void* p, size_t large_class) { if constexpr (decommit_strategy == DecommitSuperLazy) { static_assert( pal_supports, "A lazy decommit strategy cannot be implemented on platforms " "without low memory notifications"); } // Cross-reference largealloc's alloc() decommitted condition. if ( (decommit_strategy != DecommitNone) && (large_class != 0 || decommit_strategy == DecommitSuper)) { size_t rsize = bits::one_at_bit(SUPERSLAB_BITS) << large_class; memory_provider.notify_not_using( pointer_offset(p, OS_PAGE_SIZE), rsize - OS_PAGE_SIZE); } stats.superslab_push(); memory_provider.large_stack[large_class].push(static_cast(p)); } }; using GlobalVirtual = MemoryProviderStateMixin; /** * The memory provider that will be used if no other provider is explicitly * passed as an argument. */ inline GlobalVirtual& default_memory_provider() { return *(Singleton::get()); } } // namespace snmalloc