cachelib/compact_cache/CCacheVariableLruBucket.h (367 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 <folly/logging/xlog.h>
#include <algorithm>
#include <utility>
/**
* This file implements a bucket management for variable sized objects that is
* highly optimized for read operations and has a 4 byte overhead per entry + 3
* bytes per bucket.
*
* +---------------+---------------+-----------------------------------------+
* | | | |
* | | | |
* | Entry headers | Empty | Data |
* | | | |
* | | | |
* +---------------+---------------+-----------------------------------------+
*
* In this implementation, the bucket is split in three sections:
*
* 1) Entry headers.
* This section contains the entries' headers (EntryHdr). There is one
* entry header per entry in the bucket. Each entry header contains the
* following data:
* - key: the key of the entry;
* - dataSize: the size of the entry's value.
* - dataOffset: the position where to find the entry's value in the "Data"
* part of the bucket.
* The number of entries is given by the "numEntries" field of Bucket.
*
* 2) Data.
* This section contains the entries' values (EntryData). Each
* EntryData field contains:
* - headerIndex: the position of the corresponding entry header in the
* "Entry headers" section.
* - data: the value's payload. The size of this payload is determined by the
* 'dataSize' field of the corresponding entry header.
* This section has no 'holes', i.e all the entries are contiguous. The size
* of the data section is given by the "totalDataSize" field of Bucket.
*
* 3) Empty.
* This section shrinks/expands as entries are added/removed.
*
* The position of the entry headers in the "Entry headers" section determines
* the age of the entries. The right-most entry is the LRU, the left-most is the
* MRU. When an entry is promoted, the entry headers are shifted appropriately
* so that the promoted entry ends up to the left. For each entry affected by
* the shifting, the corrresponding EntryData in the "Data" section has its
* headerIndex field updated accordingly.
*
* When an entry is inserted, we check if the "Empty" section is big enough to
* hold both the new EntryHdr and the EntryData. If it is not big enough,
* we compute how many entries need to evicted in order to make enough space.
* The eviction algorithm is more expensive. Even though selecting the entries
* to be evicted is straightforward as you only need to browse them from right
* to left in the "Entry headers" section, removing them from the "Data" section
* is not trivial. Each 'block' of entries that are between two evicted entries
* are shifted to the right in order to expand the size of the "Empty" section.
* Once the necessary items have been evicted and the "Empty" section is big
* enough, the new entry's header is added to the "Entry headers" section, and
* its data is written at the beginning of the "Data" section.
*
* When an entry is deleted, the entry headers that follow the deleted entry's
* header are shifted to the left, and we update the headerIndex of the related
* EntryData. The EntryData objects that precede the deleted EntryData
* are shifted to the right. Deleting an entry expands the size of the "Empty
* section" by adding sizeof(EntryHdr) + sizeof(EntryData) + the amount of
* bytes in the payload.
*
* When an entry is updated, if the size does not change the EntryData is
* updated in place. If the new size is smaller, the end of the data is the
* same, but the beginning is moved to the right. This generates a space into
* which the preceding data is moved. If the new size is bigger, we fall back
* to deleting the entry and adding it again.
*/
namespace facebook {
namespace cachelib {
template <typename CompactCacheDescriptor>
struct VariableLruBucket {
public:
using Descriptor = CompactCacheDescriptor;
using ValueDescriptor = typename Descriptor::ValueDescriptor;
using Key = typename Descriptor::Key;
using Value = typename ValueDescriptor::Value;
static_assert(Descriptor::kHasValues,
"This bucket descriptor must be used for compact caches with"
"values.");
static_assert(!Descriptor::kValuesFixedSize,
"This bucket descriptor must be used with values of a"
"variable size.");
/** Type of the integral that contains the the size of an entry's data.
* (see dataSize in EntryHdr). */
using EntryDataSize = uint8_t;
/** Type of the integral that gives an offset (in bytes), starting from the
* 'data' field of Bucket. This is used to get the start offset of an
* EntryData object (see dataOffset in EntryHdr), and the total data
* size (in bytes) of a bucket (i.e the size of the 'Data' section). */
using EntryDataOffset = uint16_t;
/** Type of the integral that gives the index of an EntryHdr header in
* the 'Entry Headers' section (see headerIndex in EntryData), and the
* total number of entries (see numEntries in Bucket). */
using EntryNum = uint8_t;
constexpr static size_t kMaxValueSize = ValueDescriptor::kMaxSize;
/** Maximum number of entries per bucket. The number of entries must be
* small enough to fit in a value of type EntryNum, so take the max
* possible value here. */
constexpr static size_t kMaxEntries = std::numeric_limits<EntryNum>::max();
/** An entry header. */
using EntryHdr = struct {
Key key;
/* Size in bytes of the corresponding EntryData's data field. */
EntryDataSize dataSize;
/* Offset (in bytes) where to find the corresponding EntryData,
* measured from the beginning of the bucket's data field. */
EntryDataOffset dataOffset;
} __attribute__((__packed__));
using EntryData = struct {
/* Index of the corresponding EntryHdr header in the 'Entry
* Headers' section. This is a value between 0 and the number of entries
* in the bucket. */
EntryNum headerIndex;
/* Beginning of the entry's data. The size of this data is determined by
* the 'dataSize' field of the corresponding EntryHdr. */
char data[0];
} __attribute__((__packed__));
/** Compute the size of the bucket so that we can guarantee that it will be
* big enough to host an entry of the maximum size provided by the user. */
constexpr static size_t kBucketDataSize =
kMaxValueSize + sizeof(EntryHdr) + sizeof(EntryData);
using Bucket = struct {
/* Number of entries in the bucket. */
EntryNum numEntries;
/* Size of the "Data" section.
* The free space in the bucket is given by computing:
* kBucketDataSize - totalDataSize - sizeof(EntryHdr) * numEntries.
*/
EntryDataOffset totalDataSize;
/* Content of the bucket. This contains the three sections described in
* this file's documentation. */
char data[kBucketDataSize];
} __attribute__((__packed__));
/** This is to make sure that the 'totalDataSize' field of Bucket will
* never overflow. */
static_assert(kBucketDataSize <= std::numeric_limits<EntryDataOffset>::max(),
"Bucket is too big");
/**
* Handle to an entry. This class provides the compact cache implementation
* with a way to keep a reference to an entry in a bucket as well as a way
* to iterate over each valid entries without being aware of the underlying
* bucket implementation.
* This basically contains a pointer to the bucket and the index of the
* entry in the array of entry headers.
*
* Example, iterate over the valid entries in a bucket:
*
* LogStoreBucket<C>::EntryHandle h = LogStoreBucket<C>::first(myBucket);
* while (h) {
* // h refers to a valid entry in the bucket.
* // can use h.key(), h.val() to access content
* // of the entry.
* h.next();
* }
*/
class EntryHandle {
public:
explicit operator bool() const {
return bucket_ && 0 <= pos_ && pos_ < bucket_->numEntries;
}
void next() {
XDCHECK(*this);
++pos_;
}
Key key() const { return getEntry()->key; }
constexpr size_t size() const { return getEntry()->dataSize; }
Value* val() const {
return reinterpret_cast<Value*>(getEntryData(bucket_, pos_)->data);
}
EntryHandle() : bucket_(nullptr), pos_(-1) {}
EntryHandle(Bucket* bucket, int pos) : bucket_(bucket), pos_(pos) {}
bool isBucketTail() const {
return *this && pos_ == bucket_->numEntries - 1;
}
private:
EntryHdr* getEntry() const { return getEntryHeader(bucket_, pos_); }
Bucket* bucket_;
int pos_;
friend class VariableLruBucket<CompactCacheDescriptor>;
};
/** Type of the callback to be called when an entry is evicted. */
using EvictionCb = std::function<void(const EntryHandle& handle)>;
/**
* Get a handle to the first entry in the bucket.
*
* @param bucket Bucket from which to retrieve the handle.
* @return Handle to the first entry.
*/
static EntryHandle first(Bucket* bucket) {
/* If bucket->numEntries is 0, the created handle is invalid. */
return EntryHandle(bucket, 0);
}
/**
* Return the total number of items this bucket could hold.
* Due to variable size this is imprecise; extrapolate capacity
* by dividing current # of items by current fractional memory
* in use
* @param bucket Bucket to find out the number of entries it could hold
* @return number of entries this bucket can hold (approx)
*/
static uint32_t nEntriesCapacity(const Bucket& bucket) {
const size_t n = bucket.numEntries;
const size_t sz = bucket.totalDataSize;
XDCHECK_LE(sz + sizeof(EntryHdr) * n, kBucketDataSize);
if (n == 0) {
return 0;
}
return (n * kBucketDataSize) / (sz + sizeof(EntryHdr) * n);
}
/**
* Insert a new entry in a bucket.
*
* 1) Compute the required space for our entry.
* 2) Call evictEntries, which takes care of evicting as many entries as
* required in order to have the necessary space.
* 3) Allocate a new spot in the "Empty" section by increasing
* bucket->totalDataSize.
* 4) Write the entry's data (EntryData) in the new spot.
* 5) Update the headerIndex of all the existing EntryData objects
* since their header is about to get shifted one position to the
* right. This step is the aging process of the entries.
* 6) Shift all the EntryHdr headers to the right so that we can write
* our new entry header in the first position.
* 7) Write the entry's header (EntryHdr).
* 8) Set the offsets in both the EntryHdr header and the EntryData so
* that they have a reference to each other.
* 9) Update the number of entries in the bucket.
*
* @param bucket Bucket in which to insert
* @param key key of the new entry.
* @param val Value to be inserted.
* @param size Size of the value to be inserted. The size must be
* smaller than or equal to the maximum value size
* described by the value descriptor, or else this
* function will assert because the bucket might not be
* large enough for the value.
* @param evictionCb Callback to be called for when an entry is evicted.
* Cannot be empty.
* @return 0 if no item was evicted, 1 if at least one item
* was evicted, -1 on error (the given size was too
* big).
*/
static int insert(Bucket* bucket,
const Key& key,
const Value* val,
size_t size,
EvictionCb evictionCb) {
/* The caller should ensure that the value is small enough to fit in
* the bucket. */
XDCHECK_LE(size, kMaxValueSize);
if (size > kMaxValueSize) {
XLOG(ERR) << "Cannot insert an value of size " << size
<< ", the size must be smaller than " << kMaxValueSize;
return -1;
}
/* Make sure that EntryDataSize is wide enough to hold the given
* size. */
checkOverflow<EntryDataOffset>(size);
if (size > std::numeric_limits<EntryDataSize>::max()) {
XLOG(ERR)
<< "Cannot insert an value of size " << size
<< ", the size must be smaller than the max value of EntryDataSize";
return -1;
}
#ifndef NDEBUG
checkBucketConsistency(bucket);
#endif
/* 1) Compute the required space for our entry. */
size_t requiredSpace = size + sizeof(EntryHdr) + sizeof(EntryData);
/* 2) Call evictEntries, which takes care of evicting as many entries as
* required in order to have the necessary space. */
bool evicted = evictEntries(bucket, requiredSpace, evictionCb);
#ifndef NDEBUG
/* EvictEntries should leave the bucket in a consistent state. */
checkBucketConsistency(bucket);
#endif
/* 3) Allocate a new spot in the "Empty" section by increasing
* bucket->totalDataSize. */
checkOverflow<EntryDataOffset>(bucket->totalDataSize + size +
sizeof(EntryData));
bucket->totalDataSize += size + sizeof(EntryData);
/* 4) Write the entry's data (EntryData) in the new spot. */
EntryData* newEntryData = getFirstEntryData(bucket);
memcpy(newEntryData->data, val, size);
/* 5) Update the headerIndex of all the existing EntryData objects
* since their header is about to get shifted one position to the
* right. */
for (unsigned int i = 0; i < bucket->numEntries; i++) {
getEntryData(bucket, i)->headerIndex++;
}
/* 6) Shift all the EntryHdr headers to the right so that we can
* write our new entry header in the first position. */
memmove(getEntryHeader(bucket, 1),
getEntryHeader(bucket, 0),
bucket->numEntries * sizeof(EntryHdr));
/* 7) Write the entry's header (EntryHdr) */
EntryHdr* newEntry = getEntryHeader(bucket, 0);
memcpy(&newEntry->key, &key, sizeof(Key));
newEntry->dataSize = size;
/* 8) Set the offsets in both the EntryHdr header and the
* EntryData so that they have a reference to each other. */
newEntry->dataOffset = kBucketDataSize - bucket->totalDataSize;
newEntryData->headerIndex = 0;
/* 9) Update the number of entries in the bucket. */
checkOverflow<EntryNum>(bucket->numEntries + 1);
bucket->numEntries++;
#ifndef NDEBUG
checkBucketConsistency(bucket);
#endif
return evicted ? 1 : 0;
}
/**
* Promote an entry.
*
* 1) Shift all the EntryHdr headers that precede the promoted
* EntryHdr one position to the right.
* 2) Write the promoted entry to the first position.
* 3) Update the headerIndex field of all EntryData objects for which
* the corresponding EntryHdr header was moved.
*
* @param handle Handle of the entry to be promoted. Remains valid after
* this call completes.
*/
static void promote(EntryHandle& handle) {
XDCHECK(handle);
EntryHdr toPromote = *handle.getEntry();
EntryHdr* firstEntry = getEntryHeader(handle.bucket_, 0);
/* 1) Shift all the EntryHdr headers that precede the promoted
* EntryHdr one position to the right. This is the aging process. */
size_t shiftDistance = handle.pos_ * sizeof(EntryHdr);
memmove(getEntryHeader(handle.bucket_, 1), firstEntry, shiftDistance);
/* 2) Write the promoted entry to the first position. */
memcpy(firstEntry, &toPromote, sizeof(EntryHdr));
/* 3) Update the headerIndex field of all EntryData objects for
* which the corresponding EntryHdr header was moved. */
for (unsigned int i = 0; i <= handle.pos_; i++) {
getEntryData(handle.bucket_, i)->headerIndex = i;
}
/* Modify handle so that it still points to the same entry. */
handle.pos_ = 0;
#ifndef NDEBUG
checkBucketConsistency(handle.bucket_);
#endif
}
/**
* Whether an entry needs promotion. Do this if it's beyond the first
* two. The ccache usually holds at least 8 items so this should be
* reasonably safe.
*/
static inline bool needs_promote(EntryHandle& handle) {
XDCHECK(handle);
return handle.pos_ > 1;
}
/**
* Delete an entry.
*
* 1) Shift all the EntryData objects that precede the EntryData being
* deleted. Update the corresponding dataOffset fields in the EntryHdr
* headers for those moved entries.
* 2) Update the headerIndex of all the EntryData objects that
* correspond to an EntryHdr that will be shifted to the left in 3).
* 3) Shift all the EntryHdr headers that are after the deleted
* EntryHdr one position to the left.
* 4) Reduce the total size of the bucket and decrement the number of
* entries in the bucket.
*
* @param handle Handle of the entry to be deleted. After this function
* completes, the handle points to the next valid entry or
* becomes invalid if no such entry.
*/
static void del(EntryHandle& handle) {
XDCHECK(handle);
EntryDataOffset shiftDistance =
handle.getEntry()->dataSize + sizeof(EntryData);
EntryDataOffset shiftChunkSize =
handle.getEntry()->dataOffset - getFirstEntryDataOffset(handle.bucket_);
/* 1) Shift all the EntryData objects that precede the EntryData
* object of the entry we are deleting. This function also takes care of
* updating the dataOffset field of all the corresponding EntryHdr
* headers so that these remain valid. */
shiftEntriesData(handle.bucket_,
getFirstEntryData(handle.bucket_),
shiftDistance,
shiftChunkSize);
/* 2) Update the headerIndex of all the EntryData objects that
* correspond to an EntryHdr that will be shifted to the left in 3).
*/
for (unsigned int i = handle.pos_ + 1; i < handle.bucket_->numEntries;
i++) {
getEntryData(handle.bucket_, i)->headerIndex--;
}
/* 3) Shift all the EntryHdr headers that are after the deleted
* EntryHdr one position to the left. */
if (handle.pos_ < handle.bucket_->numEntries - 1) {
size_t delta = handle.bucket_->numEntries - handle.pos_ - 1;
memmove(
handle.getEntry(), handle.getEntry() + 1, delta * sizeof(EntryHdr));
}
/* 4) Reduce the total size of the bucket and decrement the number of
* entries in the bucket. */
handle.bucket_->totalDataSize -= shiftDistance;
handle.bucket_->numEntries--;
#ifndef NDEBUG
checkBucketConsistency(handle.bucket_);
#endif
}
/**
* Update the value of an entry.
*
* There are three cases:
*
* 1/ The new size is equal to the old size:
* 1) Copy the new data in place.
* 2) Promote the entry.
* 2/ The new size is smaller than the old size:
* 1) Compute the new offset of the EntryData by moving the existing
* one to the right by the amount of bytes that makes the difference
* between the old size and the new size.
* 2) Copy the new value at this offset.
* 3) Shift the EntryData object that precede our updated entry so
* that we can expand the "Empty" section by the amount of bytes
* that are not used anymore by the entry.
* 4) Update the dataOffset and dataSize fields of the EntryHdr.
* 5) Update the total size of the bucket.
* 6) Promote the entry.
* 3/ The new size is bigger.
* 1) Delete the old entry.
* 2) Insert the entry again with the new size. We don't need to promote
* the entry here because insert will insert the entry in the MRU
* spot.
* 3) Update the handle to point to the first position.
*
* @param handle Handle to the entry to be updated. Remains valid after
* this function returns.
* @param val New value of the entry.
* @param size Size of the new value.
* @param evictionCb Eviction callback to be called when an entry is
* evicted due to relocating the updated entry.
*/
static void updateVal(EntryHandle& handle,
const Value* val,
size_t size,
EvictionCb evictionCb) {
XDCHECK(handle);
EntryHdr* existingEntry = handle.getEntry();
EntryData* entryData = getEntryData(handle.bucket_, handle.pos_);
if (existingEntry->dataSize == size) {
/* The new size is equal to the old size. */
/* 1) Copy the new data in place. */
memcpy(entryData->data, val, size);
/* 2) Promote the entry. */
promote(handle);
} else if (size < existingEntry->dataSize) {
/* New data is smaller. Update the data in place starting at the new
* offset and shift the EntryData objects that precede it. */
/* 1) Compute the new offset of the EntryData by moving the
* existing one to the right by the amount of bytes that makes the
* difference between the old size and the new size. */
const EntryDataOffset shiftDistance = existingEntry->dataSize - size;
entryData = reinterpret_cast<EntryData*>(
reinterpret_cast<char*>(entryData) + shiftDistance);
/* 2) Copy the new value at this offset. */
memcpy(entryData->data, val, size);
entryData->headerIndex = handle.pos_;
/* 3) Shift the EntryData object that precede our updated entry
* so that we can expand the "Empty" section by the amount of bytes
* that are not used anymore by the entry. */
const size_t shiftChunkSize =
existingEntry->dataOffset - getFirstEntryDataOffset(handle.bucket_);
shiftEntriesData(handle.bucket_,
getFirstEntryData(handle.bucket_),
shiftDistance,
shiftChunkSize);
/* 4) Update the dataOffset and dataSize fields of the
* EntryHdr. */
existingEntry->dataOffset += shiftDistance;
existingEntry->dataSize = size;
/* 5) Update the total size of the bucket. */
handle.bucket_->totalDataSize -= shiftDistance;
/* 6) Promote the entry. */
promote(handle);
#ifndef NDEBUG
checkBucketConsistency(handle.bucket_);
#endif
} else {
/* The new size is bigger. */
XDCHECK_GT(size, existingEntry->dataSize);
/* 1) Delete the old entry. */
const EntryHdr copy = *existingEntry;
del(handle);
/* 2) Insert the entry again with the new size. */
insert(handle.bucket_, copy.key, val, size, evictionCb);
/* 3) Update the handle to point to the new position. */
handle = first(handle.bucket_);
}
}
/**
* Copy a an entry's value to a buffer.
* The buffer must be large enough to store the value, i.e the caller should
* allocate a buffer of a size greater or equal to the maximum possible size
* of a value in this compact cache.
*
* @param val Buffer in which to copy the entry's value.
* @param handle Handle of the entry from which to copy the value. Remains
* valid after this function returns.
*/
static void copyVal(Value* val, size_t* size, const EntryHandle& handle) {
XDCHECK(handle);
XDCHECK(val);
XDCHECK(size);
*size = handle.size();
const EntryData* entryData = getEntryData(handle.bucket_, handle.pos_);
memcpy(val, entryData->data, *size);
}
constexpr static size_t maxValueSize() { return kMaxValueSize; }
private:
/**
* Check that a bucket is consistent. This goes through all the entries and
* checks the offsets to verify that they match between the EntryHdr
* headers and the EntryData objects.
* This also verifies that the total sum of the sizes of all the entries is
* equal to the size of the "Data" section, and that the EntryData
* objects are contiguous.
*
* This should be called after each operation when in debug mode in order to
* verify that the operation leaves the bucket in a consistent state.
*
* @param bucket Bucket to be checked.
*/
static void checkBucketConsistency(Bucket* bucket) {
EntryHandle handle = first(bucket);
size_t totalSize = 0;
size_t nEntries = 0;
bool headerOffsetError = false;
/* Check that the data part does not overlap the entry headers. */
XDCHECK_GE(reinterpret_cast<uintptr_t>(getFirstEntryData(bucket)),
reinterpret_cast<uintptr_t>(
getEntryHeader(bucket, bucket->numEntries)));
/* Keep track of the EntryData's offsets and sizes as we see then
* when iterating on the EntryHdr headers. We will later use this to
* check that the EntryData objects are contiguous. */
using EntryInfo = std::pair<EntryDataOffset, EntryDataSize>;
std::vector<EntryInfo> entryDataSeen;
/* Iterate on the EntryHdr headers. */
while (handle) {
EntryData* entryData = getEntryData(handle.bucket_, handle.pos_);
totalSize += handle.size() + sizeof(EntryData);
/* The offset headers of the entries we are seeing should match
* their order. */
if (entryData->headerIndex != nEntries) {
headerOffsetError = true;
}
EntryHdr* entryHeader = getEntryHeader(bucket, handle.pos_);
EntryDataOffset dataOffset = entryHeader->dataOffset;
EntryDataSize dataSize = entryHeader->dataSize;
entryDataSeen.push_back(std::make_pair(dataOffset, dataSize));
handle.next();
nEntries++;
}
/* Sort entryDataSeen by offset, in increasing order. */
auto compareFn = [](const EntryInfo& a, const EntryInfo& b) -> bool {
return a.first < b.first;
};
std::sort(entryDataSeen.begin(), entryDataSeen.end(), compareFn);
/* Check that the EntryData objects are contiguous. */
bool dataOffsetError = false;
EntryDataOffset expectedNextOffset = getFirstEntryDataOffset(bucket);
for (unsigned int i = 0; i < entryDataSeen.size(); i++) {
if (entryDataSeen[i].first != expectedNextOffset) {
/* The current entry does not start where expected. */
dataOffsetError = true;
break;
}
/* The next entry should start right after the current entry. */
expectedNextOffset += entryDataSeen[i].second + sizeof(EntryData);
}
/* the last EntryData should have ended at the very end of the
* bucket. */
if (expectedNextOffset != kBucketDataSize) {
dataOffsetError = true;
}
/* Throw an assert if something is wrong. */
if (headerOffsetError || dataOffsetError ||
totalSize != bucket->totalDataSize || nEntries != bucket->numEntries) {
/* Copy the bucket locally for easier debugging in case the slab is
* not in the core dump file. */
Bucket bucketCopy;
memcpy(&bucketCopy, bucket, sizeof(Bucket));
XDCHECK(false);
}
}
/**
* Shift the EntryData objects that belong to a particular window of
* EntryData objects. This takes care of modifying the EntryHdr
* headers that correspond to each shifted EntryData object so that these
* headers have an updated dataOffset.
*
* This is used by:
* - del: When an EntryData is deleted, we need to shift the
* EntryData objects that precede it.
* - updateVal: when an EntryData is updated with a new value of a size
* smaller than the old one, the EntryData objects that precede the
* entry are shifted in order to free the space that is not used
* anymore.
* - evictEntries: when an EntryData is evicted, we need to shift the
* EntryData objects that precede it (same as del).
*
* @param bucket Bucket from which to shift data.
* @param src Pointer to the first EntryData object to be
* shifted.
* @param shiftDistance Offset to use for shifting the EntryData
* objects.
* @param shiftedChunkSize Amount of bytes to be shifted. Starting from src,
* all the EntryData objects that span in the
* window defined by this value will be shifted.
* I.e all the EntryData objects that have an
* offset between src and src + shiftDistance will
* be shifted.
*/
static void shiftEntriesData(Bucket* bucket,
EntryData* src,
EntryDataOffset shiftDistance,
size_t shiftedChunkSize) {
char* dst = (reinterpret_cast<char*>(src) + shiftDistance);
/* Adjust the dataOffset of all the EntryHdr headers that correspond
* to the EntryData objects we are going to shift. */
EntryData* cur = src;
while (reinterpret_cast<char*>(cur) <
reinterpret_cast<char*>(src) + shiftedChunkSize) {
EntryHdr* header = getEntryHeader(bucket, cur->headerIndex);
header->dataOffset += shiftDistance;
cur = getNextEntryData(bucket, cur);
}
/* Move the EntryData objects. */
memmove(dst, src, shiftedChunkSize);
}
/**
* Evict as many entries as needed, starting from the oldest, until the
* amount of free space in the bucket is equal or greater than
* requiredSpace. This is used by insert.
*
* 1) Walk through the EntryHdr headers starting from the LRU while
* updating a counter that contains the memory made available if evicted
* all entries seen so far. Stop when this counter is high enough (i.e
* bigger than requiredSpace).
* 2) Sort the list of entries we decided to evict by their offset in the
* "Data" section, in reverse order. The cost of sorting the evicted
* entries should be negligible since the number of evicted entries
* should be small.
* 3) Move the "blocks" of EntryData objects that are between two evicted
* entries, one by one.
* 4) reduce the number of entries and the total size of the entries in the
* bucket.
*
* @param bucket Bucket from which to evict entries.
* @param requiredSpace Amount of space required (in bytes).
* @param evictionCb callback to be called for each evicted entry.
* @return true if at least one entry was evicted.
*/
static bool evictEntries(Bucket* bucket,
size_t requiredSpace,
EvictionCb evictionCb) {
/* Will contain all the entries we decide to evict. */
std::vector<EntryHdr*> arr;
size_t freeSpace = kBucketDataSize - bucket->totalDataSize -
sizeof(EntryHdr) * bucket->numEntries;
if (freeSpace >= requiredSpace && bucket->numEntries != kMaxEntries) {
/* We already have enough space. */
return false;
}
/* 1) Walk through the EntryHdr headers starting from the LRU while
* updating a counter that contains the memory made available if
* evicted all entries seen so far. Stop when this counter is high
* enough (i.e bigger than requiredSpace). */
/* Start from the lru, which is the last entry in the entry header. */
unsigned int evictedPos = bucket->numEntries - 1;
/* Convenient lambda function for evicting an entry. */
auto evictOneMoreEntryFn = [&]() {
EntryHdr* entry = getEntryHeader(bucket, evictedPos);
/* There should always be at least one entry for eviction until
* we have enough space. */
XDCHECK_GE(reinterpret_cast<uintptr_t>(entry),
reinterpret_cast<uintptr_t>(getEntryHeader(bucket, 0)));
/* Evicting the entry releases the space of the entry data and the
* space of the entry header. */
freeSpace += entry->dataSize + sizeof(EntryHdr) + sizeof(EntryData);
arr.push_back(entry);
evictionCb(EntryHandle(bucket, evictedPos));
evictedPos--;
};
/* Evict at least one entry if the current number of entries is about to
* overflow. This should not happen very often.
* If this happens often, this means that the bucket is too big for the
* entries, and we are wasting a lot of memory. */
if (bucket->numEntries == kMaxEntries) {
XLOG(ERR) << "Overflow in number of entries in a bucket";
evictOneMoreEntryFn();
}
/* Evict entries until we have enough space. */
while (freeSpace < requiredSpace) {
evictOneMoreEntryFn();
}
/* 2) Sort the list of entries we decided to evict by their offset in
* the "Data" section, in increasing order, so that the left-most
* evicted entry is the first element of the array, and the right-most
* evected entry is the last element.
* The cost of sorting the evicted entries should be negligible since
* the number of evicted entries should be small. */
auto compareFn = [](const EntryHdr* a, const EntryHdr* b) -> bool {
return a->dataOffset < b->dataOffset;
};
XDCHECK_GT(arr.size(), 0);
std::sort(arr.begin(), arr.end(), compareFn);
/* 3) Move the "chunks" of EntryData objects that are between two
* evicted entries, one by one. */
size_t shiftDistance = 0;
EntryDataOffset chunkStartOffset;
int i = 0;
for (i = arr.size() - 1; i >= 0; i--) {
/* Amount of bytes by which to shift the chunk. We are removing the
* current entry 'arr[i]', so we shift by the total amount of bytes
* used by its corresponding EntryData. Here, we increment
* shiftDistance instead of simply setting it to a new value because
* we want to take into account the space made available by the
* shifting performed by the previous iterations of this loop. */
shiftDistance += arr[i]->dataSize + sizeof(EntryData);
/* Compute the start offset of the chunk we are moving.
* The chunk starts right at the end of the removed entry that is
* at the left of the current removed entry. If the current removed
* entry is the left-most, the start offset of the chunk is the
* beginning of the data. */
if (i == 0) {
chunkStartOffset = getFirstEntryDataOffset(bucket);
} else {
chunkStartOffset =
arr[i - 1]->dataOffset + arr[i - 1]->dataSize + sizeof(EntryData);
}
/* Size of the chunk to be moved. */
size_t shiftedChunkSize = arr[i]->dataOffset - chunkStartOffset;
if (shiftedChunkSize == 0) {
/* The current removed entry is contiguous with the removed
* entry on the left, so there is nothing between them to be
* moved. */
continue;
}
/* Pointer to the beginning of that chunk. */
EntryData* src =
reinterpret_cast<EntryData*>(bucket->data + chunkStartOffset);
shiftEntriesData(bucket, src, shiftDistance, shiftedChunkSize);
}
/* 4) reduce the number of entries and the total size of the entries in
* the bucket. */
XDCHECK_GE(bucket->numEntries, arr.size());
XDCHECK_GE(bucket->totalDataSize, shiftDistance);
bucket->numEntries -= arr.size();
bucket->totalDataSize -= shiftDistance;
return true;
}
/* Utility functions */
/**
* Get the offset where the "Data" section of the given bucket starts.
*
* @param bucket Bucket for which we want to compute the offset where the
* data section begins.
* @return Offset where the "Data" section of the bucket starts.
*/
constexpr static size_t getFirstEntryDataOffset(Bucket* bucket) {
return kBucketDataSize - bucket->totalDataSize;
}
/**
* Return a pointer to the first EntryData object in a bucket.
*
* @param bucket Bucket from which to retrieve the first EntryData.
* @return Pointer to the first EntryData object in the bucket.
*/
constexpr static EntryData* getFirstEntryData(Bucket* bucket) {
return reinterpret_cast<EntryData*>(bucket->data +
getFirstEntryDataOffset(bucket));
}
/**
* Return a pointer to the EntryHdr header at position headerPos.
*
* @param bucket Bucket from which to retrieve the EntryHdr header.
* @param headerPos Position of the entry header.
* @return Pointer to the EntryHdr header at position headerPos.
*/
constexpr static EntryHdr* getEntryHeader(Bucket* bucket, uint8_t headerPos) {
return reinterpret_cast<EntryHdr*>(bucket->data) + headerPos;
}
/**
* Get a pointer to the entry data pointed to by the entry header at
* position headerPos.
* @param bucket Bucket in which to look for the data.
* @param headerPos Position of the header of the entry for which to
* retrieve the data.
* @return Pointer to the entry data for the given entry header.
*/
constexpr static EntryData* getEntryData(Bucket* bucket, uint8_t headerPos) {
return reinterpret_cast<EntryData*>(
bucket->data + getEntryHeader(bucket, headerPos)->dataOffset);
}
/*
* Get the EntryData object that follows a given EntryData object.
* It's the caller's responsibility to check that the returned pointer
* does not overflow the bucket.
* This is called by shiftEntriesData.
*
* @param bucket Bucket from which to find the bucket EntryData object.
* @param entryData Entry data for which we want to find the next one.
* @return Entry data that follows entryData.
*/
constexpr static EntryData* getNextEntryData(Bucket* bucket,
EntryData* entryData) {
return reinterpret_cast<EntryData*>(
reinterpret_cast<char*>(entryData) + sizeof(EntryData) +
getEntryHeader(bucket, entryData->headerIndex)->dataSize);
}
/**
* Check that the given value will not overflow if written to an integral of
* type T.
*/
template <typename T>
static void checkOverflow(size_t val) {
XDCHECK_LE(val, std::numeric_limits<T>::max());
}
};
template <typename CompactCacheDescriptor>
constexpr size_t VariableLruBucket<CompactCacheDescriptor>::kBucketDataSize;
template <typename CompactCacheDescriptor>
constexpr size_t VariableLruBucket<CompactCacheDescriptor>::kMaxValueSize;
} // namespace cachelib
} // namespace facebook