cachelib/experimental/objcache/Allocator.h (147 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 <cstddef>
#include <memory>
#include <scoped_allocator>
#include "cachelib/allocator/CacheAllocator.h"
#include "cachelib/common/Exceptions.h"
namespace facebook {
namespace cachelib {
namespace objcache {
// Allocator that conforms to the std::allocator interface. This is meant
// to be a thin shim that wraps the various cachelib-backed allocators
// underneath. This allocator is stateful and two allocators may compare equal
// only if the underlying allocator resource is equal. We also explicitly
// disable propagation on copy-assignment, move-assignment, and swap in order
// to prevent user from mixing cachelib-memory with jemalloc-memory.
//
// The relationship of components in this file is as follows:
// Allocator
// |
// AllocatorResource
// |
// <implementation of a cachelib-backed allocator>
//
// The reason we use private inheritance is to avoid the allocator resource
// taking up unnecessary space in case it is empty.
template <typename T, typename AllocatorResource>
class Allocator : private AllocatorResource {
public:
using value_type = T;
using propagate_on_container_copy_assignment = std::false_type;
using propagate_on_container_move_assignment = std::false_type;
using propagate_on_container_swap = std::false_type;
Allocator() = default;
explicit Allocator(AllocatorResource resource)
: AllocatorResource{resource} {}
// We allow copy construction from an allocator of a different value_type and
// AllocatorResource, as long as the AllocatorResource is compatible. E.g. all
// cachelib-backed AllocatorResource must have the same type of ItemHandle.
template <typename T2, typename AllocatorResource2>
friend class Allocator;
template <typename T2, typename AllocatorResource2>
/* implicit */ Allocator(
const Allocator<T2, AllocatorResource2>& other) noexcept
: AllocatorResource{static_cast<const AllocatorResource2&>(other)} {}
// @param size the size of the allocation in units of sizeof(T)
// @return a valid pointer pointing at the allocation with at
// least (size * sizeof(T)) bytes
// @throw ObjectCacheAllocationError on failure
value_type* allocate(size_t size) {
size_t bytes = size * sizeof(value_type);
return reinterpret_cast<value_type*>(
static_cast<AllocatorResource*>(this)->allocate(
bytes, std::alignment_of<value_type>()));
}
// @param alloc a valid pointer pointing the allocation to be freed
// @param size size of the allocation. It must be the same as the
// original requested size in units of sizeof(T)
// @throw ObjectCacheDeallocationBadArgs if bad arguments
void deallocate(value_type* alloc, size_t size) {
size_t bytes = size * sizeof(value_type);
static_cast<AllocatorResource*>(this)->deallocate(
alloc, bytes, std::alignment_of<value_type>());
}
AllocatorResource& getAllocatorResource() {
return *static_cast<AllocatorResource*>(this);
}
template <typename U, typename V>
friend bool operator==(const Allocator<T, AllocatorResource>& a,
const Allocator<U, V>& b) noexcept {
return a.isEqual(b);
}
template <typename U, typename V>
friend bool operator!=(const Allocator<T, AllocatorResource>& a,
const Allocator<U, V>& b) noexcept {
return !(a == b);
}
};
// Base class for a cachelib-backed allocator. This component enfores
// the same interface across all cachelib-backed allocator, and also
// takes care of equality-comparison between different allocators. In
// addition, in the case of an "empty" AllocatorResource, this will
// fall back to global allocator.
template <typename Impl, typename CacheDescriptor>
class AllocatorResource {
public:
template <typename Impl2, typename CacheDescriptor2>
friend class AllocatorResource;
using ItemHandle = typename CacheDescriptor::ItemHandle;
using Item = typename CacheDescriptor::Item;
using ChainedItem = typename CacheDescriptor::ChainedItem;
AllocatorResource() = default;
explicit AllocatorResource(ItemHandle* hdl) : hdl_{hdl} { XDCHECK(*hdl_); }
template <typename Impl2, typename CacheDescriptor2>
explicit AllocatorResource(
const AllocatorResource<Impl2, CacheDescriptor2>& other) noexcept
: hdl_{other.hdl_} {}
// @param bytes the size of the allocation in units of bytes
// @param alignment align the returning address to this
// @return a valid pointer pointing at the allocation with at
// least requested bytes
// @throw ObjectCacheAllocationError on failure
void* allocate(size_t bytes, size_t alignment) {
if (!hdl_) {
return allocateFallback(bytes, alignment);
}
return static_cast<Impl*>(this)->allocateImpl(bytes, alignment);
}
// @param alloc a valid pointer pointing the allocation to be freed
// @param size size of the allocation. It must be the same as the
// original requested bytes
// @param alignment the alloc must be aligned to this
// @throw ObjectCacheDeallocationBadArgs if bad arguments
void deallocate(void* alloc, size_t bytes, size_t alignment) {
if (!hdl_) {
deallocateFallback(alloc, bytes, alignment);
return;
}
return static_cast<Impl*>(this)->deallocateImpl(alloc, bytes, alignment);
}
// @return true if two instances of AllocatorResource is compatible,
// false otherwise.
template <typename Impl2, typename CacheDescriptor2>
bool isEqual(
const AllocatorResource<Impl2, CacheDescriptor2>& other) const noexcept {
return hdl_ == other.hdl_;
}
private:
// Fall-back allocation and deallocation when this allocator is NOT associated
// with a cache item. We use the global allocator for fallback.
void* allocateFallback(size_t bytes, size_t alignment);
void deallocateFallback(void* alloc, size_t bytes, size_t alignment);
protected:
// TODO: can we use some form of a compressed pointer? As-is, each allocator
// is 8 bytes. This means for small structures that need allocator, this
// can be a hefty space overhead.
//
// Pointer to the item handle. The actual storage is located in the parent
// item's memory. Note that this handle is a special one. It does not have
// any outstanding refcount. The reason we need a handle is to access
// CacheAllocator APIs for allocating additional chained items.
ItemHandle* hdl_{};
};
// Traits that describe what cache we use that underlies our allocator
template <typename CacheT>
struct CacheDescriptor {
using Cache = CacheT;
using ItemHandle = typename CacheT::ItemHandle;
using Item = typename CacheT::Item;
using ChainedItem = typename CacheT::ChainedItem;
};
template <typename CacheDescriptor>
class MonotonicBufferResource
: public AllocatorResource<MonotonicBufferResource<CacheDescriptor>,
CacheDescriptor> {
public:
using Base = AllocatorResource<MonotonicBufferResource<CacheDescriptor>,
CacheDescriptor>;
using ItemHandle = typename Base::ItemHandle;
using Item = typename Base::Item;
using ChainedItem = typename Base::ChainedItem;
// Metadata for this allocator. This is located in the beginning of a cache
// item associated with this allocator.
struct FOLLY_PACK_ATTR Metadata {
// This is a one-byte value for us to detect this is a cachelib-backed
// allocator. This is best effort since it is possible an arbitrary
// user-created blob in cache could also contain this byte.
uint16_t magicCookie{0xbeef};
// TODO: consider updating CacheAllocator to allow us using an "item"
// for manipulating chained items. That would mean we no longer
// have to store "item handle" but just a pair of pointers to
// the item and the cache allocator. That would require 16 bytes
// compared to the 40 bytes right now.
// Storaged for item handle we need for accessing CacheAllocator API
uint8_t itemHandleStorage[sizeof(ItemHandle)]{};
// Number of bytes currently used by structures backed by this allocator
uint32_t usedBytes{};
// Remaining bytes that can be allocated for new allocations
uint32_t remainingBytes{};
// Pointer to the beginning of free buffer
uint8_t* buffer{};
};
static_assert(58 == sizeof(Metadata), "Incorrect size for metadata");
static constexpr uint32_t metadataSize() { return sizeof(Metadata); }
// Undefined behavior if the allocator was not initialized with a
// reserved storage
static void* getReservedStorage(void* memory, size_t alignment) {
auto ptr = reinterpret_cast<uintptr_t>(memory);
ptr += metadataSize();
ptr = (ptr + alignment - 1) / alignment * alignment;
return reinterpret_cast<void*>(ptr);
}
using AllocatorResource<MonotonicBufferResource<CacheDescriptor>,
CacheDescriptor>::AllocatorResource;
// Implement AllocatorResource::allocate
void* allocateImpl(size_t bytes, size_t alignment);
// Implement AllocatorResource::deallocate
void deallocateImpl(void* alloc, size_t byte, size_t alignment);
// Return a read-only view of metdata. Useful for tests and debugging.
const Metadata* viewMetadata() const {
return const_cast<MonotonicBufferResource*>(this)->getMetadata();
}
private:
Metadata* getMetadata() {
XDCHECK(this->hdl_);
return reinterpret_cast<Metadata*>((*this->hdl_)->getMemory());
}
// Allocate from existing storage in the allocator
// @return a valid pointer pointing at the allocation with at least requested
// bytes. Nullptr if insufficient storage.
void* allocateFast(size_t bytes, size_t alignment);
// Allocate additional storage from Cache to satisfy this allocation
// @return a valid pointer pointing at the allocation with at
// least requested bytes
// @throw AllocationError on failure
void* allocateSlow(size_t bytes, size_t alignment);
};
// Create a new monotonic allocator
//
// @param cache cache where we allocate bytes for this allocator
// @param poolId which cache pool we will be using for this allocator
// @param key key associated with this allocator
// @param reservedBytes storage reserved for user-manipulation, which
// will not be used for allocation
// @param additionalBytes additional storage in the parent item, which
// will be used to serve the initial allocations
// @param alignment align reservedBytes
//
// @return [ItemHandle, MonotonicBufferResource] if successful
// The lifetime of this allocator is tied to the ItemHandle.
// The allocator itself holds no reference and can only be used
// if there is at least one outstanding item handle in scope.
// @throw ObjectCacheAllocationError on failure
template <typename MonotonicBufferResource, typename Cache>
std::pair<typename Cache::ItemHandle, MonotonicBufferResource>
createMonotonicBufferResource(Cache& cache,
PoolId poolId,
folly::StringPiece key,
uint32_t reservedBytes,
uint32_t additionalBytes,
size_t alignment);
} // namespace objcache
} // namespace cachelib
} // namespace facebook
#include "cachelib/experimental/objcache/Allocator-inl.h"