cachelib/compact_cache/CCacheFixedLruBucket.h (123 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
/**
* This file implements a bucket management that uses entries of a fixed size
* that are stored contiguously in the bucket (linear probing).
*
* When an entry is added, the entries are shifted down one position and the new
* entries is written in the first position. If the last position contained an
* entry, it is evicted.
*
* This implementation provides an lru mechanism by moving the promoted entry to
* the top and shifting all the entries that were above it down one position.
*/
namespace facebook {
namespace cachelib {
template <typename CompactCacheDescriptor, unsigned EntriesPerBucket>
struct FixedLruBucket {
public:
using Descriptor = CompactCacheDescriptor;
using ValueDescriptor = typename Descriptor::ValueDescriptor;
using Key = typename Descriptor::Key;
using Value = typename ValueDescriptor::Value;
constexpr static int kEntriesPerBucket = EntriesPerBucket;
constexpr static bool kHasValues = Descriptor::kHasValues;
static_assert(Descriptor::kValuesFixedSize,
"This bucket descriptor must be used with values of a fixed"
"size");
/** Type of the data stored in a bucket entry.
* This contains the key and the value (if any). */
using Entry = struct {
Key key;
/* Expands to NoValue (size 0) if this cache does not store values */
Value val;
} __attribute__((__packed__));
/** Type of a bucket.
* Empty entry slots must be zeroed out to avoid spurious matches! */
using Bucket = struct {
Entry entries[kEntriesPerBucket];
} __attribute__((__packed__));
static_assert(sizeof(Entry) == sizeof(Key) + sizeof(Value),
"Entry packing went awry");
static_assert(sizeof(Bucket) == kEntriesPerBucket * sizeof(Entry),
"Bucket packing went awry");
/**
* 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 entry offset.
*
* Example, iterate over the valid entries in a bucket:
*
* FixedLruBucket<C>::EntryHandle h = FixedLruBucket<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:
/** Return true if the handle is valid, i.e it points to an non-empty
* entry or we went passed the last entry. */
explicit operator bool() const {
return pos_ >= 0 && pos_ < kEntriesPerBucket &&
!bucket_->entries[pos_].key.isEmpty();
}
/** Move to the next entry. The handle will become invalid after
* reaching the last entry, or when reaching an empty entry.
* Must be called on a valid handle. */
void next() {
XDCHECK(*this);
++pos_;
}
Key key() const { return bucket_->entries[pos_].key; }
Value* val() const { return &bucket_->entries[pos_].val; }
constexpr size_t size() const { return sizeof(Value); }
EntryHandle() : bucket_(nullptr), pos_(-1) {}
EntryHandle(Bucket* bucket, int pos) : bucket_(bucket), pos_(pos) {}
bool isBucketTail() const { return *this && pos_ == kEntriesPerBucket - 1; }
private:
Bucket* bucket_;
int pos_;
friend struct FixedLruBucket<CompactCacheDescriptor, EntriesPerBucket>;
};
/** Type of the callback to be called when an entry is evicted. */
using EvictionCb = std::function<void(const EntryHandle& handle)>;
/**
* Return a handle to the first entry in the bucket.
*
* @param bucket Bucket from which to retrieve a handle to the first entry.
* @return Handle to the first entry, or invalid handle if the bucket
* is empty.
*/
static EntryHandle first(Bucket* bucket) {
/* If the first entry is empty, this creates an invalid handle. */
return EntryHandle(bucket, 0);
}
/**
* Return capacity of this bucket in number of entries
* @param bucket
* @return number of entries this bucket can hold when filled
*/
static uint32_t nEntriesCapacity(const Bucket& /*bucket*/) {
return kEntriesPerBucket;
}
/*
* Insert a new entry in a bucket.
* The entry is written in the first position after we shift all the entries
* down one position, evicting the last one if any.
*
* @param bucket Bucket in which to insert the new entry.
* @param key key of the new entry.
* @param val Pointer to a value to be copied in the new
* entry. Unused if Value Type is NoValue.
* @param size Unused because the size of values is already known
* in a compact cache that stores values of a fixed
* size.
* @param evictionCb Callback to be called for when an entry is evicted.
* Cannot be empty.
* @return 1 if an entry was evicted, 0 otherwise.
*/
static bool insert(Bucket* bucket,
const Key& key,
const Value* val,
size_t,
EvictionCb evictionCb) {
bool evicted = false;
/* The last position is the LRU. If its key is not zero (i.e there is
* an entry at the position), evict it. */
if (!bucket->entries[kEntriesPerBucket - 1].key.isEmpty()) {
/* We need to evict an entry. */
XDCHECK(evictionCb);
evictionCb(EntryHandle(bucket, kEntriesPerBucket - 1));
evicted = true;
}
/* Slide all the entries down one position. */
memmove(&bucket->entries[1],
&bucket->entries[0],
sizeof(Entry) * (kEntriesPerBucket - 1));
/* Write our new entry in top of bucket. */
memcpy(&bucket->entries[0].key, &key, sizeof(Key));
if (kHasValues) {
copyValue(&bucket->entries[0].val, val);
}
return evicted ? 1 : 0;
}
/**
* Promote an entry.
*
* @param handle Handle of the entry to be promoted. Remains valid after
* this method returns, and points to the next location of the
* entry.
*/
static void promote(EntryHandle& handle) {
XDCHECK(handle);
if (handle.pos_ != 0) {
Entry winner = handle.bucket_->entries[handle.pos_];
memmove(&handle.bucket_->entries[1],
&handle.bucket_->entries[0],
sizeof(Entry) * handle.pos_);
memcpy(&handle.bucket_->entries[0], &winner, sizeof(Entry));
handle.pos_ = 0;
}
}
static inline bool needs_promote(EntryHandle& handle) {
XDCHECK(handle);
if (handle.pos_ > kEntriesPerBucket / 4) {
return true;
}
return false;
}
/**
* Delete an entry.
* We simply shift all the entries after it one position up.
*
* @param handle Handle of the entry to be deleted. After this method
* returns, the handle will point to the next entry, if any,
* or becomes invalid.
*/
static void del(EntryHandle& handle) {
XDCHECK(handle);
memmove(&handle.bucket_->entries[handle.pos_],
&handle.bucket_->entries[handle.pos_ + 1],
sizeof(Entry) * (kEntriesPerBucket - handle.pos_ - 1));
bzero(&handle.bucket_->entries[kEntriesPerBucket - 1], sizeof(Entry));
}
/**
* Update the value of an entry.
*
* @param handle Handle of the entry to be updated. Remains valid after
* this function returns.
* @param val New value of the entry.
* @param size Unused because the size of values is already known in a
* compact cache that stores values of a fixed size.
* @param evictionCb Eviction callback to be called if this operation evicts
* an entry. This is unused because this implementation
* does not cause entries to be evicted when updating.
*/
static void updateVal(EntryHandle& handle,
const Value* val,
size_t,
EvictionCb /*evictionCb*/) {
if (kHasValues) {
XDCHECK(val);
copyValue(handle.val(), val);
}
}
/**
* Copy a an entry's value to a buffer.
* @param val Buffer in which to copy the entry's value.
* @param size Unused because the size of values is already known in a
* compact cache that stores values of a fixed size.
* @param handle Entry from which to copy the value.
*/
static void copyVal(Value* val, size_t*, EntryHandle& handle) {
XDCHECK(handle);
XDCHECK(val);
copyValue(val, handle.val());
}
private:
/**
* Copy a value from one buffer to another.
* The caller should ensure that this is called with non NULL values.
* The source and destination buffers must not overlap.
*
* otherwise this function will assert.
* @param destPtr Pointer to the destination buffer.
* @param srcPtr Pointer to the source buffer.
*/
template <typename T>
static void copyValue(T* destPtr, const T* srcPtr) {
XDCHECK(destPtr != nullptr);
XDCHECK(srcPtr != nullptr);
memcpy(destPtr, srcPtr, sizeof(T));
}
};
} // namespace cachelib
} // namespace facebook