blob: 2f10f09ec79ea8255cc1221376cd7557ead586f1 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
**/
#include "storage/EvictionPolicy.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "storage/StorageBlockInfo.hpp"
#include "storage/StorageConstants.hpp"
#include "threading/Mutex.hpp"
#include "threading/SpinMutex.hpp"
#include "threading/SpinSharedMutex.hpp"
#include "utility/Macros.hpp"
#include "glog/logging.h"
using std::chrono::duration_cast;
using std::chrono::high_resolution_clock;
using std::chrono::time_point;
using std::chrono::microseconds;
namespace quickstep {
EvictionPolicy::Status EvictAnyBlockEvictionPolicy::chooseBlockToEvict(block_id* block) {
MutexLock lock(mutex_);
if (nonreferenced_blocks_.empty()) {
return Status::kBlockNotFound;
}
std::unordered_set<block_id>::const_iterator it = nonreferenced_blocks_.begin();
*block = *it;
ref_counts_.erase(*it);
nonreferenced_blocks_.erase(it);
return Status::kOk;
}
void EvictAnyBlockEvictionPolicy::blockCreated(const block_id block) {
MutexLock lock(mutex_);
ref_counts_[block] = 0;
nonreferenced_blocks_.insert(block);
}
void EvictAnyBlockEvictionPolicy::blockEvicted(const block_id block) {
MutexLock lock(mutex_);
ref_counts_.erase(block);
nonreferenced_blocks_.erase(block);
}
void EvictAnyBlockEvictionPolicy::blockReferenced(const block_id block) {
MutexLock lock(mutex_);
if (ref_counts_[block]++ == 0) {
// Since the ref count was 0, the block might be in nonreferenced_blocks_
nonreferenced_blocks_.erase(block);
}
}
void EvictAnyBlockEvictionPolicy::blockUnreferenced(const block_id block) {
MutexLock lock(mutex_);
DEBUG_ASSERT(ref_counts_.find(block) != ref_counts_.end());
DEBUG_ASSERT(ref_counts_[block] > 0);
if (--ref_counts_[block] == 0) {
nonreferenced_blocks_.insert(block);
}
}
unsigned int EvictAnyBlockEvictionPolicy::getRefCount(const block_id block) {
MutexLock lock(mutex_);
DEBUG_ASSERT(ref_counts_.find(block) != ref_counts_.end());
return ref_counts_[block];
}
EvictionPolicy::Status UniformRandomEvictionPolicy::chooseBlockToEvict(block_id* block) {
MutexLock lock(mutex_);
if (nonreferenced_blocks_.empty()) {
return Status::kBlockNotFound;
}
std::uniform_int_distribution<> rng(0, nonreferenced_blocks_.size() - 1);
std::vector<block_id>::iterator block_it = nonreferenced_blocks_.begin() + rng(gen_);
*block = *block_it;
ref_counts_.erase(*block);
nonreferenced_blocks_.erase(block_it);
return Status::kOk;
}
void UniformRandomEvictionPolicy::blockCreated(const block_id block) {
MutexLock lock(mutex_);
ref_counts_[block] = 0;
nonreferenced_blocks_.push_back(block);
}
void UniformRandomEvictionPolicy::blockEvicted(const block_id block) {
MutexLock lock(mutex_);
if (ref_counts_.erase(block) != 0) {
// The only way a block could be in nonreferenced_blocks_ is if it were
// first referenced and then later unreferenced. Thus, we only need to
// check nonreferenced_blocks_ if it had an entry in ref_counts_.
// Further, if it had an entry in ref_counts_, it must have an entry in
// nonreferenced_blocks_, unless it was evicted with a non-zero ref count,
// which is an error.
std::vector<block_id>::iterator it = std::find(nonreferenced_blocks_.begin(), nonreferenced_blocks_.end(), block);
DEBUG_ASSERT(it != nonreferenced_blocks_.end());
nonreferenced_blocks_.erase(it);
}
}
void UniformRandomEvictionPolicy::blockReferenced(const block_id block) {
MutexLock lock(mutex_);
std::unordered_map<block_id, unsigned int>::iterator it = ref_counts_.find(block);
if (it == ref_counts_.end()) {
ref_counts_[block] = 1;
} else if (++(it->second) == 1) {
// If we already had a ref count of 0 for the block, then it must be in nonreferenced_blocks_, since that could
// only happen if the block were referenced and then unreferenced back to 0.
std::vector<block_id>::iterator it = std::find(nonreferenced_blocks_.begin(), nonreferenced_blocks_.end(), block);
DEBUG_ASSERT(it != nonreferenced_blocks_.end());
nonreferenced_blocks_.erase(it);
}
}
void UniformRandomEvictionPolicy::blockUnreferenced(const block_id block) {
MutexLock lock(mutex_);
DEBUG_ASSERT(ref_counts_.find(block) != ref_counts_.end());
DEBUG_ASSERT(ref_counts_[block] > 0);
if (--ref_counts_[block] == 0) {
nonreferenced_blocks_.push_back(block);
}
}
unsigned int UniformRandomEvictionPolicy::getRefCount(const block_id block) {
MutexLock lock(mutex_);
DEBUG_ASSERT(ref_counts_.find(block) != ref_counts_.end());
return ref_counts_[block];
}
/**
* @brief This class provides the implementation of the LRU-k eviction
* policy. It is a template parameterized on K, the number of references to keep
* for each block.
**/
template<std::size_t K>
class LRUKEvictionPolicyImpl : public EvictionPolicy {
public:
explicit LRUKEvictionPolicyImpl(const microseconds correlated_reference_period);
~LRUKEvictionPolicyImpl() override { }
EvictionPolicy::Status chooseBlockToEvict(block_id* block) override;
void blockCreated(const block_id block) override;
void blockEvicted(const block_id block) override;
void blockReferenced(const block_id block) override;
void blockUnreferenced(const block_id block) override;
void blockDeleted(const block_id block) override;
unsigned int getRefCount(const block_id block) override;
private:
struct BlockInfo {
/**
* Array of the last K references. ref_list_[0] refers to the most recent
* access, ref_list_[1] to the second most recent, and so on. This only
* records references that did not take place within the same Correlated
* Reference Period. These times are drawn from
* std::chrono::high_resolution_clock.
**/
std::array<time_point<high_resolution_clock>, K> ref_list_;
/**
* Time of the last reference regardless of whether it occurred within the
* Correlated Reference Period of the previous reference.
*/
time_point<high_resolution_clock> last_reference_;
/**
* How many references are outstanding on this block.
*/
std::atomic<unsigned int> ref_count_;
/**
* Whether or not this block is currently in memory.
*/
std::atomic<bool> in_memory_;
/**
* A mutex protecting ref_list_ and last_reference_.
*/
mutable SpinSharedMutex<false> time_mutex_;
BlockInfo()
: ref_count_(0), in_memory_(false) {
ref_list_.fill(time_point<high_resolution_clock>::min());
}
/**
* @brief Increments the reference count for a block, marks the block as
* in memory, and records a new reference time.
*
* @param correlated_reference_period The correlated reference period of
* the parent LRU-K eviction policy.
* @return True if reference count was zero before this call (meaning the
* caller should erase this block from nonreferenced_blocks_).
**/
bool blockReferencedHelper(const microseconds correlated_reference_period) {
time_point<high_resolution_clock> time = high_resolution_clock::now();
SpinSharedMutexExclusiveLock<false> lock(time_mutex_);
if (in_memory_.load()) {
// Block was already in buffer pool.
if (time - last_reference_ > correlated_reference_period) {
addReferenceTime(time, true);
}
} else {
// Block was just brought into the buffer pool, possibly after an
// eviction.
addReferenceTime(time, false);
in_memory_.store(true);
}
last_reference_ = time;
return (ref_count_++ == 0);
}
private:
/**
* @brief Adds a new time as the most recent non-correlated reference. Does
* not update last_reference_. Also adjusts the previous reference
* times by moving them up
* @param time The new time to add.
**/
inline void addReferenceTime(const time_point<high_resolution_clock> &time,
const bool currently_in_memory) {
// Move existing reference times back while also adjusting them for the
// correlated reference period of this block
if (currently_in_memory) {
const microseconds corrl_period = duration_cast<microseconds>(last_reference_ - ref_list_[0]);
for (int i = ref_list_.size() - 1; i > 0; --i) {
ref_list_[i] = ref_list_[i - 1] + corrl_period;
}
} else {
for (int i = ref_list_.size() - 1; i > 0; --i) {
ref_list_[i] = ref_list_[i - 1];
}
}
// Now add the new reference to the front
ref_list_[0] = time;
}
DISALLOW_COPY_AND_ASSIGN(BlockInfo);
};
/**
* @brief Compares two BlockInfo objects A and B as follows:
* (1) If the time since A's last reference is greater than the correlated reference period and B's is not,
* A < B
* (2) Otherwise, A < B iff A's Kth most recent reference < B's Kth most recent reference.
* @return true iff lhs < rhs
*/
class BlockInfoComparator {
public:
const time_point<high_resolution_clock> time_;
const microseconds correlated_reference_period_;
BlockInfoComparator(
const time_point<high_resolution_clock> time,
const microseconds correlated_reference_period)
: time_(time), correlated_reference_period_(correlated_reference_period) { }
inline bool operator()(const BlockInfo& lhs, const BlockInfo& rhs) {
time_point<high_resolution_clock> lhs_last, rhs_last, lhs_next_last, rhs_next_last;
{
SpinSharedMutexSharedLock<false> lhs_lock(lhs.time_mutex_);
lhs_last = lhs.last_reference_;
lhs_next_last = lhs.ref_list_[K - 1];
}
{
SpinSharedMutexSharedLock<false> rhs_lock(rhs.time_mutex_);
rhs_last = rhs.last_reference_;
rhs_next_last = rhs.ref_list_[K - 1];
}
bool lhs_past_corrl_period = time_ - lhs_last > correlated_reference_period_;
bool rhs_past_corrl_period = time_ - rhs_last > correlated_reference_period_;
if (lhs_past_corrl_period == rhs_past_corrl_period) {
return lhs_next_last < rhs_next_last;
} else {
return lhs_past_corrl_period;
}
}
};
std::unordered_map<block_id, BlockInfo> blocks_;
alignas(kCacheLineBytes) SpinSharedMutex<false> blocks_mutex_;
const microseconds correlated_reference_period_;
std::unordered_set<block_id> nonreferenced_blocks_;
SpinMutex nonreferenced_blocks_mutex_;
#ifdef QUICKSTEP_DEBUG
/**
* @brief Prints the contents of the LRUKEvictionPolicyImpl. Use this
* strictly for debugging purposes.
* WARNING: This method is not safe as it does not acquire
* any mutexes on the internal state variable, so use it
* primarily for debugging purposes.
*
* @param print_map If true, print the eviction map.
* @param print_nr_list If true, print the list of non referenced blocks.
*
*/
void printState(const bool print_map, const bool print_nr_list) const {
if (print_map) {
LOG(INFO) << "Eviction Policy Map (" << blocks_.size() << " entries: ";
for (const auto& it : blocks_) {
LOG(INFO) << "[" << it.first // print the block number
<< ", " << it.second.ref_count_ << "] "; // and the ref count
}
LOG(INFO) << "\n";
}
if (print_nr_list) {
// Print the non-referenced block list
LOG(INFO) << "Non referenced block list is: ";
for (const block_id nonref_block : nonreferenced_blocks_) {
LOG(INFO) << nonref_block << "; ";
}
LOG(INFO) << "\n";
}
}
#endif
DISALLOW_COPY_AND_ASSIGN(LRUKEvictionPolicyImpl);
};
template<std::size_t K>
LRUKEvictionPolicyImpl<K>::LRUKEvictionPolicyImpl(
const microseconds correlated_reference_period)
: correlated_reference_period_(correlated_reference_period) { }
template<std::size_t K>
EvictionPolicy::Status LRUKEvictionPolicyImpl<K>::chooseBlockToEvict(block_id* block) {
const BlockInfo *smallest_blockinfo = nullptr;
BlockInfoComparator cmp(high_resolution_clock::now(), correlated_reference_period_);
SpinSharedMutexSharedLock<false> blocks_read_lock(blocks_mutex_);
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
block_id ret;
{
for (const block_id nonref_block : nonreferenced_blocks_) {
const BlockInfo &blockinfo = blocks_[nonref_block];
if (blockinfo.in_memory_.load()) {
if ((smallest_blockinfo == nullptr || cmp(blockinfo, *smallest_blockinfo))) {
smallest_blockinfo = &blockinfo;
ret = nonref_block;
}
}
}
}
if (smallest_blockinfo == nullptr) {
return Status::kBlockNotFound;
} else {
*block = ret;
#ifdef QUICKSTEP_DEBUG
if (blocks_[*block].ref_count_ != 0) {
LOG(INFO) << "Block " << *block
<< " has nonzero reference count. Printing state...\n";
printState(true, true);
}
#endif
DEBUG_ASSERT(blocks_[*block].ref_count_ == 0);
blocks_[*block].in_memory_.store(false);
nonreferenced_blocks_.erase(*block);
return Status::kOk;
}
}
template<std::size_t K>
void LRUKEvictionPolicyImpl<K>::blockCreated(const block_id block) {
SpinSharedMutexExclusiveLock<false> blocks_write_lock(blocks_mutex_);
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
// 'blocks_[block]' will be implicitly default-constructed below. Set
// 'in_memory_' to true initially.
blocks_[block].in_memory_.store(true);
// Block is initially nonreferenced.
nonreferenced_blocks_.insert(block);
}
template<std::size_t K>
void LRUKEvictionPolicyImpl<K>::blockEvicted(const block_id block) {
SpinSharedMutexSharedLock<false> blocks_read_lock(blocks_mutex_);
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
DEBUG_ASSERT(blocks_[block].ref_count_ == 0);
blocks_[block].in_memory_.store(false);
nonreferenced_blocks_.erase(block);
}
template<std::size_t K>
void LRUKEvictionPolicyImpl<K>::blockReferenced(const block_id block) {
bool block_known = false;
{
SpinSharedMutexSharedLock<false> read_lock(blocks_mutex_);
typename std::unordered_map<block_id, BlockInfo>::iterator it = blocks_.find(block);
if (it != blocks_.end()) {
block_known = true;
// Need to protect the access to "ref_count", so grab the mutex.
// Note we need to grab this outside the loop as "blockReferencedHelper"
// changes the "ref_count."
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
if (it->second.blockReferencedHelper(correlated_reference_period_)) {
nonreferenced_blocks_.erase(block);
}
}
}
if (!block_known) {
SpinSharedMutexExclusiveLock<false> blocks_write_lock(blocks_mutex_);
// As above, need to protect access to ref_count, so grab the mutex.
// Note we need to grab this outside the loop as "blockReferencedHelper"
// changes the "ref_count."
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
if (blocks_[block].blockReferencedHelper(correlated_reference_period_)) {
nonreferenced_blocks_.erase(block);
}
}
}
template<std::size_t K>
void LRUKEvictionPolicyImpl<K>::blockUnreferenced(const block_id block) {
SpinSharedMutexSharedLock<false> read_lock(blocks_mutex_);
DEBUG_ASSERT(blocks_.find(block) != blocks_.end());
DEBUG_ASSERT(blocks_[block].ref_count_ > 0);
BlockInfo& blockinfo = blocks_[block];
// Need to protect the access to "ref_count", so grab the mutex.
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
if (--blockinfo.ref_count_ == 0) {
nonreferenced_blocks_.insert(block);
}
}
template<std::size_t K>
void LRUKEvictionPolicyImpl<K>::blockDeleted(const block_id block) {
SpinSharedMutexExclusiveLock<false> write_lock(blocks_mutex_);
SpinMutexLock nonreferenced_blocks_lock(nonreferenced_blocks_mutex_);
blocks_.erase(block);
nonreferenced_blocks_.erase(block);
}
template<std::size_t K>
unsigned int LRUKEvictionPolicyImpl<K>::getRefCount(const block_id block) {
SpinSharedMutexSharedLock<false> read_lock(blocks_mutex_);
DEBUG_ASSERT(blocks_.find(block) != blocks_.end());
return blocks_[block].ref_count_.load();
}
EvictionPolicy* LRUKEvictionPolicyFactory::ConstructLRUKEvictionPolicy(
const size_t k,
const microseconds correlated_reference_period) {
switch (k) {
case 1:
return new LRUKEvictionPolicyImpl<1>(
correlated_reference_period);
break;
case 2:
return new LRUKEvictionPolicyImpl<2>(
correlated_reference_period);
break;
case 3:
return new LRUKEvictionPolicyImpl<3>(
correlated_reference_period);
break;
default:
FATAL_ERROR("LRU-k only supports k = 1, 2, or 3");
}
}
} // namespace quickstep