cachelib/allocator/Refcount.h (215 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/CPortability.h>
#include <folly/Likely.h>
#include <folly/logging/xlog.h>
#include <folly/portability/Asm.h>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include "cachelib/common/CompilerUtils.h"
#include "cachelib/common/Exceptions.h"
namespace facebook {
namespace cachelib {
// refcount and flag in the CacheItem.
class FOLLY_PACK_ATTR RefcountWithFlags {
public:
/**
* Layout of refcount type. This includes the flags, admin ref bits, and
* refcount. Admin ref encodes special reference counts that contain
* information on who holds the reference count (like AccessContainer or
* MMContainer). Flags encode item state such as whether or not it's
* a chained item.
*
* The layout is as follows.
* |-- flags --|-- admin ref --|-- access ref--|
* - Flags: 11 bits
* - Admin Ref: 3 bits
* - Access Ref: 18 bits
*/
using Value = uint32_t;
static_assert(std::is_unsigned<Value>::value,
"Unsigned Integral type required");
static constexpr uint8_t kNumFlags = 11;
static constexpr uint8_t kNumAdminRefBits = 3;
static constexpr uint8_t kNumAccessRefBits = 18;
static_assert(kNumAccessRefBits <= NumBits<Value>::value, "Invalid type");
static_assert(kNumAccessRefBits >= 1, "Need at least one bit for refcount");
static_assert(
NumBits<Value>::value == kNumFlags + kNumAdminRefBits + kNumAccessRefBits,
"Value must be exactly the number of all the bits added together.");
static constexpr Value kAccessRefMask = std::numeric_limits<Value>::max() >>
(NumBits<Value>::value -
kNumAccessRefBits);
static constexpr Value kRefMask = std::numeric_limits<Value>::max() >>
(NumBits<Value>::value - kNumAdminRefBits -
kNumAccessRefBits);
/**
* Access reference counts indicate wthere there are an outstanding
* references, but not who owns it. These control bits are special refcounts
* that enable Cache operations to infer who owns a reference count and take
* appropriate action in the lifetime management of the Item within the cache.
*/
enum AdminRef {
// is linked through memory container
kLinked = kNumAccessRefBits,
// exists in hash table
kAccessible,
// this flag indicates the allocation is being moved elsewhere
// (can be triggered by a resize or reblanace operation)
kMoving,
};
/**
* Flags indicating item state. These are not used in the eviction or moving
* synchronization. They are used to indicate what sort of item this is in
* cache operations. For example, kNvmClean == false is the initial state of
* an item in ram cache. It means we'll need to insert it into the NVM cache.
* When we load an item from NVM cache, kNvmClean is set to true. However,
* after user mutates the content, they will invalidate the copy of the item
* in NVM cache and sets the clean bit to false.
*/
enum Flags {
// 3 bits reserved for MMContainer
kMMFlag0 = kNumAccessRefBits + kNumAdminRefBits,
kMMFlag1,
kMMFlag2,
// Whether or not an item is a regular item or chained alloc
kIsChainedItem,
// If a regular item has chained allocs
kHasChainedItem,
// Item hasn't been modified after loading from nvm
kNvmClean,
// Item was evicted from NVM while it was in RAM.
kNvmEvicted,
// A deprecated and noop flag that was used to mark whether the item is
// unevictable in the past.
kUnevictable_NOOP,
// Unused. This is just to indciate the maximum number of flags
kFlagMax,
};
static_assert(static_cast<uint8_t>(kMMFlag0) > static_cast<uint8_t>(kMoving),
"Flags and control bits cannot overlap in bit range.");
static_assert(kFlagMax <= NumBits<Value>::value, "Too many flags.");
constexpr explicit RefcountWithFlags() = default;
RefcountWithFlags(const RefcountWithFlags&) = delete;
RefcountWithFlags& operator=(const RefcountWithFlags&) = delete;
RefcountWithFlags(RefcountWithFlags&&) = delete;
RefcountWithFlags& operator=(RefcountWithFlags&&) = delete;
// Bumps up the reference count only if the new count will be strictly less
// than or equal to the maxCount.
// @return true if refcount is bumped. false otherwise.
FOLLY_ALWAYS_INLINE bool incRef() noexcept {
Value* const refPtr = &refCount_;
unsigned int nCASFailures = 0;
constexpr bool isWeak = false;
Value oldVal = __atomic_load_n(refPtr, __ATOMIC_RELAXED);
while (true) {
const Value newCount = oldVal + static_cast<Value>(1);
if (UNLIKELY((oldVal & kAccessRefMask) == (kAccessRefMask))) {
return false;
}
if (__atomic_compare_exchange_n(refPtr, &oldVal, newCount, isWeak,
__ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
return true;
}
if ((++nCASFailures % 4) == 0) {
// this pause takes up to 40 clock cycles on intel and the lock cmpxchgl
// above should take about 100 clock cycles. we pause once every 400
// cycles or so if we are extremely unlucky.
folly::asm_volatile_pause();
}
}
}
// Bumps down the reference count
//
// @return Refcount with control bits. When it is zero, we know for
// certain no one has access to it.
// @throw RefcountUnderflow when we are trying to decremenet from 0
// refcount and have a refcount leak.
FOLLY_ALWAYS_INLINE Value decRef() {
Value* const refPtr = &refCount_;
unsigned int nCASFailures = 0;
constexpr bool isWeak = false;
Value oldVal = __atomic_load_n(refPtr, __ATOMIC_RELAXED);
while (true) {
const Value newCount = oldVal - static_cast<Value>(1);
if ((oldVal & kAccessRefMask) == 0) {
throw exception::RefcountUnderflow(
"Trying to decRef with no refcount. RefCount Leak!");
}
if (__atomic_compare_exchange_n(refPtr, &oldVal, newCount, isWeak,
__ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
return newCount & kRefMask;
}
if ((++nCASFailures % 4) == 0) {
// this pause takes up to 40 clock cycles on intel and the lock cmpxchgl
// above should take about 100 clock cycles. we pause once every 400
// cycles or so if we are extremely unlucky
folly::asm_volatile_pause();
}
}
}
// Return refcount excluding control bits and flags
Value getAccessRef() const noexcept { return getRaw() & kAccessRefMask; }
// Return access ref and the admin ref bits
Value getRefWithAccessAndAdmin() const noexcept {
return getRaw() & kRefMask;
}
// Returns the raw refcount with control bits and flags
Value getRaw() const noexcept {
// This used to be just 'return refCount_'.
// On intel it's the same as an atomic load on primitive types,
// but it's better to be explicit and do an atomic load with
// relaxed ordering.
return __atomic_load_n(&refCount_, __ATOMIC_RELAXED);
}
/**
* The following three functions correspond to the state of the allocation
* in the memory management container. This is protected by the
* MMContainer's internal locking. Inspecting this outside the mm
* container will be racy.
*/
void markInMMContainer() noexcept {
Value bitMask = getAdminRef<kLinked>();
__atomic_or_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
void unmarkInMMContainer() noexcept {
Value bitMask = ~getAdminRef<kLinked>();
__atomic_and_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
bool isInMMContainer() const noexcept {
return getRaw() & getAdminRef<kLinked>();
}
/**
* The following three functions correspond to the state of the allocation
* in the access container. This will be protected by the access container
* lock. Depending on their state outside of the access container might be
* racy
*/
void markAccessible() noexcept {
Value bitMask = getAdminRef<kAccessible>();
__atomic_or_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
void unmarkAccessible() noexcept {
Value bitMask = ~getAdminRef<kAccessible>();
__atomic_and_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
bool isAccessible() const noexcept {
return getRaw() & getAdminRef<kAccessible>();
}
/**
* The following four functions are used to track whether or not
* an item is currently in the process of being moved. This happens during a
* slab rebalance or resize operation.
*
* An item can only be marked moving when `isInMMContainer` returns true.
* This operation is atomic.
*
* User can also query if an item "isOnlyMoving". This returns true only
* if the refcount is 0 and only the moving bit is set.
*
* Unmarking moving does not depend on `isInMMContainer`
*/
bool markMoving() noexcept {
Value bitMask = getAdminRef<kMoving>();
Value conditionBitMask = getAdminRef<kLinked>();
Value* const refPtr = &refCount_;
unsigned int nCASFailures = 0;
constexpr bool isWeak = false;
Value curValue = __atomic_load_n(refPtr, __ATOMIC_RELAXED);
while (true) {
const bool flagSet = curValue & conditionBitMask;
if (!flagSet) {
return false;
}
const Value newValue = curValue | bitMask;
if (__atomic_compare_exchange_n(refPtr, &curValue, newValue, isWeak,
__ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
XDCHECK(newValue & conditionBitMask);
return true;
}
if ((++nCASFailures % 4) == 0) {
// this pause takes up to 40 clock cycles on intel and the lock cmpxchgl
// above should take about 100 clock cycles. we pause once every 400
// cycles or so if we are extremely unlucky.
folly::asm_volatile_pause();
}
}
}
void unmarkMoving() noexcept {
Value bitMask = ~getAdminRef<kMoving>();
__atomic_and_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
bool isMoving() const noexcept { return getRaw() & getAdminRef<kMoving>(); }
bool isOnlyMoving() const noexcept {
// An item is only moving when its refcount is zero and only the moving bit
// among all the control bits is set. This indicates an item is already on
// its way out of cache and does not need to be moved.
auto ref = getRefWithAccessAndAdmin();
bool anyOtherBitSet = ref & ~getAdminRef<kMoving>();
if (anyOtherBitSet) {
return false;
}
return ref & getAdminRef<kMoving>();
}
/**
* Item cannot be marked both chained allocation and
* marked as having chained allocations at the same time
*/
void markIsChainedItem() noexcept { setFlag<kIsChainedItem>(); }
void unmarkIsChainedItem() noexcept { unSetFlag<kIsChainedItem>(); }
bool isChainedItem() const noexcept { return isFlagSet<kIsChainedItem>(); }
void markHasChainedItem() noexcept { setFlag<kHasChainedItem>(); }
void unmarkHasChainedItem() noexcept { unSetFlag<kHasChainedItem>(); }
bool hasChainedItem() const noexcept { return isFlagSet<kHasChainedItem>(); }
/**
* Keep track of whether the item was modified while in ram cache
*/
void markNvmClean() noexcept { return setFlag<kNvmClean>(); }
void unmarkNvmClean() noexcept { return unSetFlag<kNvmClean>(); }
bool isNvmClean() const noexcept { return isFlagSet<kNvmClean>(); }
/**
* Marks that the item was potentially evicted from the nvmcache and might
* need to be rewritten even if it was nvm-clean
*/
void markNvmEvicted() noexcept { return setFlag<kNvmEvicted>(); }
void unmarkNvmEvicted() noexcept { return unSetFlag<kNvmEvicted>(); }
bool isNvmEvicted() const noexcept { return isFlagSet<kNvmEvicted>(); }
// Whether or not an item is completely drained of access
// Refcount is 0 and the item is not linked, accessible, nor moving
bool isDrained() const noexcept { return getRefWithAccessAndAdmin() == 0; }
// Whether or not we hold the last exclusive access to this item
// Refcount is 1 and the item is not linked, accessible, nor moving
bool isExclusive() const noexcept { return getRefWithAccessAndAdmin() == 1; }
/**
* Functions to set, unset and check flag presence. This is exposed
* for external components to manipulate flags. E.g. MMContainer will
* need to access reserved flags for MM.
* We will not allow anyone to explicitly manipulate control bits via
* these functions (compiler error).
*/
template <Flags flagBit>
void setFlag() noexcept {
static_assert(flagBit >= kNumAccessRefBits + kNumAdminRefBits,
"incorrect flag");
static_assert(flagBit < NumBits<Value>::value, "incorrect flag");
constexpr Value bitMask = (static_cast<Value>(1) << flagBit);
__atomic_or_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
template <Flags flagBit>
void unSetFlag() noexcept {
static_assert(flagBit >= kNumAccessRefBits + kNumAdminRefBits,
"incorrect flag");
static_assert(flagBit < NumBits<Value>::value, "incorrect flag");
constexpr Value bitMask =
std::numeric_limits<Value>::max() - (static_cast<Value>(1) << flagBit);
__atomic_and_fetch(&refCount_, bitMask, __ATOMIC_ACQ_REL);
}
template <Flags flagBit>
bool isFlagSet() const noexcept {
return getRaw() & getFlag<flagBit>();
}
private:
template <Flags flagBit>
static Value getFlag() noexcept {
static_assert(flagBit >= kNumAccessRefBits + kNumAdminRefBits,
"incorrect flag");
static_assert(flagBit < NumBits<Value>::value, "incorrect flag");
return static_cast<Value>(1) << flagBit;
}
template <AdminRef adminRef>
static Value getAdminRef() noexcept {
static_assert(adminRef >= kNumAccessRefBits, "incorrect control bit");
static_assert(adminRef < kNumAccessRefBits + kNumAdminRefBits,
"incorrect control bit");
return static_cast<Value>(1) << adminRef;
}
// why not use std::atomic? Because std::atomic does not work well with
// padding and requires alignment.
__attribute__((__aligned__(alignof(Value)))) Value refCount_{0};
};
} // namespace cachelib
} // namespace facebook