cachelib/allocator/memory/MemoryPool.h (124 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <atomic> #include <cstddef> #include <memory> #include <mutex> #include <vector> #include "cachelib/allocator/memory/AllocationClass.h" #include "cachelib/allocator/memory/MemoryAllocatorStats.h" #include "cachelib/allocator/memory/Slab.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wconversion" #include "cachelib/allocator/memory/serialize/gen-cpp2/objects_types.h" #pragma GCC diagnostic pop namespace facebook { namespace cachelib { namespace tests { class AllocTestBase; } // namespace tests class SlabAllocator; // memory pool corresponds to a pool set up by the MemoryPoolManager. All // allocations for a given memory pool go through this. It consists of a bunch // of allocation classes and talks to the slab allocator for grabbing slabs. The // active memory pool size indicates the amount of memory actively being used // by the memory pool. The sum of active memory and freed slabs and freed // allocations per allocation class should amount for the total memory footprint // of this memory pool from the slab allocator's perspective. class MemoryPool { public: // creates a pool with the id and size. // // @param id the unique pool id. // @param poolSize max size of the pool. // @param alloc the slab allocator for requesting the slabs. // @param allocSizes the set of allocation class sizes for this pool, // sorted in increasing order. The largest size should be // less than Slab::kSize. // @throw std::invalid_argument if allocSizes is invalid MemoryPool(PoolId id, size_t poolSize, SlabAllocator& alloc, const std::set<uint32_t>& allocSizes); // creates a pool by restoring it from a serialized buffer. // @param object Object that contains the data to restore MemoryPool // @param alloc the slab allocator for fetching the header info. // @throw std::invalid_argument if the object state is invalid. // std::logic_error if the Memory pool is not compatible for // restoration with the slab allocator. MemoryPool(const serialization::MemoryPoolObject& object, SlabAllocator& alloc); MemoryPool(const MemoryPool&) = delete; MemoryPool& operator=(const MemoryPool&) = delete; // returns the poolId of this memory pool. PoolId getId() const noexcept { return id_; } // the configured size of the pool. size_t getPoolSize() const noexcept { return maxSize_; } // return the current size of the pool that has been already advised. Note // include the configured advised size if advising has not // caught up. size_t getPoolAdvisedSize() const noexcept { return curSlabsAdvised_ * Slab::kSize; } // return the usable size of the pool. // Usable pool size is configured pool size minus advised away size size_t getPoolUsableSize() const noexcept { auto advisedSize = getPoolAdvisedSize(); return maxSize_ <= advisedSize ? 0 : maxSize_ - advisedSize; } // returns the allocation class sizes as configured in this pool const std::vector<uint32_t>& getAllocSizes() const noexcept { return acSizes_; } // returns true if the memory pools has more memory allocated than the // current size. This is possible because we allow resizing the pool // dynamically. bool overLimit() const noexcept { auto getCurrentUsedAndAdvisedSize = getCurrentUsedSize() + getPoolAdvisedSize(); return getCurrentUsedAndAdvisedSize > maxSize_; } // returns the size of memory currently unallocated in this pool size_t getUnAllocatedSlabMemory() const noexcept { auto totalAllocSize = currSlabAllocSize_ + getPoolAdvisedSize(); return totalAllocSize > maxSize_ ? 0 : maxSize_ - totalAllocSize; } // returns the current memory that is allocated in this memory pool. size_t getCurrentAllocSize() const noexcept { return currAllocSize_; } // returns current memory used by this memory pool, including slabs // in the free list. size_t getCurrentUsedSize() const noexcept; // returns true if the pool has allocated all the slabs it can for some // allocation class. This does not mean that the pool is full since the // allocation class corresponding to some allocation class can still have // free memory available. bool allSlabsAllocated() const noexcept { auto currAdvisedSize = getPoolAdvisedSize(); return (currSlabAllocSize_ + currAdvisedSize + Slab::kSize) > maxSize_; } MPStats getStats() const; // allocates memory of at least _size_ bytes. // // @param size size of the allocation. // @return pointer to allocation or nullptr on failure to allocate. // @throw std::invalid_argument if size is invalid. void* allocate(uint32_t size); // Allocate a slab with zeroed memory // // @return pointer to allocation or nullptr on failure to allocate. // @throw std::invalid_argument if requestedSize is invalid. void* allocateZeroedSlab(); // frees the memory back to the pool. throws an exception if the memory does // not belong to this pool. // // @param memory pointer to the memory to be freed // // throws the following exceptions in cases where either the caller freed // the wrong allocations to this pool or if there is an internal corruption // of data structures // @throw std::invalid_argument if the memory does not belong to this pool. // @throw std::run_time_error if the slab class information is corrupted. void free(void* memory); // resize the memory pool. This only adjusts the Pool size. It does not // release the slabs back to the SlabAllocator if the new size is less than // the current size. The caller is responsible for doing that through // startSlabRelease() calls. // // @param size_t size the new size for this pool. void resize(size_t size) noexcept { maxSize_ = size; } // Start the process of releasing a slab from this allocation class id and // pool id. The release could be for a pool resizing or allocation class // rebalancing. If a valid context is returned, the caller needs to free the // active allocations in the valid context and call completeSlabRelease. A // null context indicates that a slab was successfully released. throws on // any other error. // // @param victim the allocation class id in the pool. if invalid, we try // to pick from the free slabs if available // @param receiver the allocation class that will get a slab // @param mode the mode for slab release (rebalance/resize) // @param hint hint referring to the slab. this can be an allocation that // the user knows to exist in the slab. If this is nullptr, a // random slab is selected from the pool and allocation class. // @param zeroOnRelease whether or not to zero out the slab // @param shouldAbortFn invoked in the code to see if this release slab // process should be aborted // // @return a valid context. If the slab is already released, then the // caller needs to do nothing. If it is not released, then the caller // needs to free the allocations and call completeSlabRelease with // the same context. // // @throw std::invalid_argument if the hint is invalid or if the pid or cid // is invalid. Or if the mode is set to kResize but the receiver is // also specified. Receiver class id can only be specified if the mode // is set to kRebalance. // @throw exception::SlabReleaseAborted if slab release is aborted due to // shouldAbortFn returning true. SlabReleaseContext startSlabRelease( ClassId victim, ClassId receiver, SlabReleaseMode mode, const void* hint, bool zeroOnRelease, SlabReleaseAbortFn shouldAbortFn = []() { return false; }); // Aborts the slab release process when there were active allocations in // the slab. This should be called with the same non-null context that was // created using startSlabRelease and after the user FAILS to free all the // active allocations in the context. The state of the allocation class may // not exactly same as pre-startSlabRelease state because freed allocations // while trying to release the slab are not restored. // // @param context context returned by startSlabRelease // // @throw std::invalid_argument if the context is invalid or // context is already released or all allocs in the context are // free void abortSlabRelease(const SlabReleaseContext& context); // completes the slab release process when there were active allocations in // the slab. This should be called with the same non-null context that was // created using startSlabRelease and after the user frees all the active // allocations in the context. After this, the slab is released // appropriately. This will block until all the allocations are returned to // the allocator. // // @param context context returned by startSlabRelease // // @throw std::invalid_argument if the context is invalid. // Or if the mode is set to kResize but the receiver is // also specified. Receiver class id can only be specified if the mode // is set to kRebalance. void completeSlabRelease(const SlabReleaseContext& context); // Reclaim the given number of advised away slabs for this pool from // the slab allocator. // // @param numSlabs the maximum number of slab to be reclaimed. // @return the actual number of slabs reclaimed by the pool size_t reclaimSlabsAndGrow(size_t numSlabs); // for saving the state of the memory pool // // precondition: The object must have been instantiated with a restorable // slab allocator that does not own the memory. serialization must happen // without any reader or writer present. Any modification of this object // afterwards will result in an invalid, inconsistent state for the // serialized data. // // @throw std::logic_error if the object state can not be serialized serialization::MemoryPoolObject saveState() const; // fetch the ClassId corresponding to the allocation class from this memory // pool // // @param size the allocation size // @return the allocations class id corresponding to the alloc size // @throw std::invalid_argument if the size does not correspond to any // allocation class. ClassId getAllocationClassId(uint32_t size) const; // fetch the ClassId for the memory. // // @param memory pointer to allocated memory from this pool. // @return the allocation class id for the memory // @throw std::invalid_argument if the memory does not belong to this pool // or a valid allocation class. ClassId getAllocationClassId(const void* memory) const; // fetch the allocation class for inspection. This is merely to read the // info about the allocation class. // // @param cid the allocation class id that we are looking for. // @return pointer to the AllocationClass. guaranteed to be valid // allocation class. // @throw std::invalid_argument if the ClassId is invalid. const AllocationClass& getAllocationClass(ClassId cid) const; // return the number of allocation ClassIds for this pool based on the // allocation sizes that it was configured with. All allocations from this // pool will have ClassId from [0 .. numClassId - 1] (inclusive). unsigned int getNumClassId() const noexcept { return static_cast<unsigned int>(acSizes_.size()); } // Gets allocation class for a given class id and calls forEachAllocation on // that allocation class. // // @param callback Callback to be executed on each allocation // // @return true to conitnue with iteration, false to abort. template <typename AllocTraversalFn> bool forEachAllocation(ClassId classId, Slab* slab, AllocTraversalFn&& callback) { auto& allocClass = getAllocationClassFor(classId); return allocClass.forEachAllocation( slab, std::forward<AllocTraversalFn>(callback)); } // returns the number of slabs currently advised away uint64_t getNumSlabsAdvised() const { return curSlabsAdvised_; } // set the number of slabs advised away. This is called only when // we have no slabs to advise away or reclaim but number of slabs // advised in across the pools need to be rebalanced. // // @param value new value for the curSlabsAdvised_ void setNumSlabsAdvised(uint64_t value) { curSlabsAdvised_ = value; } private: // container for storing a vector of AllocationClass. using ACVector = std::vector<std::unique_ptr<AllocationClass>>; // intended to be used by the constructor to verify the state of the memory // pool, specifically when we deserialize from a serialized state // @throw std::invalid_argument if any of the state is invalid. void checkState() const; // get a slab for use based on the activeSize and maxSize. returns nullptr // if out of slab memory. Slab* getSlabLocked() noexcept; // create allocation classes corresponding to the pool's configuration. ACVector createAllocationClasses() const; // @return AllocationClass corresponding to the memory, if it // belongs to an AllocationClass // // @throw std::invalid_argument if the memory does not belong to this pool // or is invalid. AllocationClass& getAllocationClassFor(void* memory) const; // fetch the allocation class corresponding to the allocation size. returns // // @param size the allocation size requested. // @return allocation class. // @throw std::invalid_argument if the allocation size is out of range. AllocationClass& getAllocationClassFor(uint32_t size) const; // fetch the allocation class corresponding to the class id. // // @param cid the allocation class id // @return the allocation class // @throw std::invalid_argument if the class id is invalid. AllocationClass& getAllocationClassFor(ClassId cid) const; // helper function to release a slab back to either the slab allocator or to // our free pool. // @param mode the mode of the release operation // @param slab the slab to be released. // @param zeroOnRelease whether or not to zero out the slab // @param receiverClassId optional AC to receive this slab void releaseSlab(SlabReleaseMode mode, const Slab* slab, bool zeroOnRelease, ClassId receiverClassId); // create a slab release context from the free slabs if possible. // // @throw std::invalid_argument if there are no free slabs available. SlabReleaseContext releaseFromFreeSlabs(); // mutex for serializing access to freeSlabs_ and the currSlabAllocSize_. mutable std::mutex lock_; // the id for this memory pool const PoolId id_{-1}; // the current max size of the memory pool. std::atomic<size_t> maxSize_{0}; // the current size of all the slab memory we have allocated for this pool // that actively belong to one of its AllocationClasses. This does not // include the memory under freeSlabs_. std::atomic<size_t> currSlabAllocSize_{0}; // the current size of all allocations from this memory pool. // currAllocSize_ <= currSlabSize_ <= maxSize_ std::atomic<size_t> currAllocSize_{0}; // the allocator for slabs. SlabAllocator& slabAllocator_; // slabs allocated from the slab allocator for this memory pool, that are // not currently in use. std::vector<Slab*> freeSlabs_; // sorted vector of allocation class sizes const std::vector<uint32_t> acSizes_; // vector of allocation classes for this pool, sorted by their allocation // sizes and indexed by their class id. This vector does not change once it // is initialized inside the constructor. so this can be accessed without // grabbing the mutex. const ACVector ac_; // Current configuration of advised away Slabs in the pool std::atomic<uint64_t> curSlabsAdvised_{0}; // number of slabs we released for resizes and rebalances std::atomic<unsigned int> nSlabResize_{0}; std::atomic<unsigned int> nSlabRebalance_{0}; std::atomic<unsigned int> nSlabReleaseAborted_{0}; static std::vector<uint32_t> createMcSizesFromSerialized( const serialization::MemoryPoolObject& object); static ACVector createMcFromSerialized( const serialization::MemoryPoolObject& object, PoolId poolId, SlabAllocator& alloc); // Allow access to private members by unit tests friend class facebook::cachelib::tests::AllocTestBase; }; } // namespace cachelib } // namespace facebook