2017-04-09 05:19:50 +00:00
|
|
|
#pragma once
|
|
|
|
|
|
|
|
#include <atomic>
|
|
|
|
#include <mutex>
|
|
|
|
#include <list>
|
|
|
|
#include <memory>
|
2017-04-09 07:02:13 +00:00
|
|
|
#include <random>
|
2017-04-09 05:19:50 +00:00
|
|
|
#include <unordered_map>
|
|
|
|
#include <sys/mman.h>
|
|
|
|
#include <boost/intrusive/list.hpp>
|
|
|
|
#include <boost/intrusive/set.hpp>
|
|
|
|
#include <boost/noncopyable.hpp>
|
2017-06-06 17:18:32 +00:00
|
|
|
#include <ext/scope_guard.h>
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
#include <Common/Exception.h>
|
2017-04-09 07:02:13 +00:00
|
|
|
#include <Common/randomSeed.h>
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-06-20 04:49:00 +00:00
|
|
|
#ifndef MAP_ANONYMOUS
|
|
|
|
#define MAP_ANONYMOUS MAP_ANON
|
|
|
|
#endif
|
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
namespace DB
|
|
|
|
{
|
|
|
|
namespace ErrorCodes
|
|
|
|
{
|
|
|
|
extern const int CANNOT_ALLOCATE_MEMORY;
|
|
|
|
extern const int CANNOT_MUNMAP;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Cache for variable length memory regions (contiguous arrays of bytes).
|
|
|
|
* Example: cache for data read from disk, cache for decompressed data, etc.
|
|
|
|
* It combines cache and allocator: allocates memory by itself without use of malloc/new.
|
|
|
|
*
|
|
|
|
* Motivation:
|
|
|
|
* - cache has specified memory usage limit and we want this limit to include allocator fragmentation overhead;
|
|
|
|
* - the cache overcomes memory fragmentation by cache eviction;
|
|
|
|
* - there is no sense for reclaimed memory regions to be cached internally by usual allocator (malloc/new);
|
|
|
|
* - by allocating memory directly with mmap, we could place it in virtual address space far
|
|
|
|
* from other (malloc'd) memory - this helps debugging memory stomping bugs
|
|
|
|
* (your program will likely hit unmapped memory and get segfault rather than silent cache corruption)
|
|
|
|
*
|
|
|
|
* Implementation:
|
|
|
|
*
|
|
|
|
* Cache is represented by list of mmapped chunks.
|
|
|
|
* Each chunk holds free and occupied memory regions. Contiguous free regions are always coalesced.
|
|
|
|
*
|
2017-04-09 06:29:26 +00:00
|
|
|
* Each region could be linked by following metadata structures:
|
2017-04-11 02:52:22 +00:00
|
|
|
* 1. LRU list - to find next region for eviction. NOTE Replace with exponentially-smoothed size-weighted LFU map.
|
2017-04-09 05:19:50 +00:00
|
|
|
* 2. Adjacency list - to find neighbour free regions to coalesce on eviction.
|
|
|
|
* 3. Size multimap - to find free region with at least requested size.
|
|
|
|
* 4. Key map - to find element by key.
|
|
|
|
*
|
|
|
|
* Each region has:
|
|
|
|
* - size;
|
|
|
|
* - refcount: region could be evicted only if it is not used anywhere;
|
|
|
|
* - chunk address: to check if regions are from same chunk.
|
|
|
|
*
|
|
|
|
* During insertion, each key is locked - to avoid parallel initialization of regions for same key.
|
|
|
|
*
|
|
|
|
* On insertion, we search for free region of at least requested size.
|
2017-04-11 02:52:22 +00:00
|
|
|
* If nothing was found, we evict oldest unused region; if not enogh size, we evict it neighbours; and try again.
|
2017-04-09 05:19:50 +00:00
|
|
|
*
|
|
|
|
* Metadata is allocated by usual allocator and its memory usage is not accounted.
|
|
|
|
*
|
|
|
|
* Caveats:
|
|
|
|
* - cache is not NUMA friendly.
|
2017-04-11 02:52:22 +00:00
|
|
|
*
|
|
|
|
* Performance: few million ops/sec from single thread, less in case of concurrency.
|
|
|
|
* Fragmentation is usually less than 10%.
|
2017-04-09 05:19:50 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
template <typename Key, typename Payload>
|
|
|
|
class ArrayCache : private boost::noncopyable
|
|
|
|
{
|
|
|
|
private:
|
|
|
|
struct LRUListTag;
|
|
|
|
struct AdjacencyListTag;
|
|
|
|
struct SizeMultimapTag;
|
|
|
|
struct KeyMapTag;
|
|
|
|
|
|
|
|
using LRUListHook = boost::intrusive::list_base_hook<boost::intrusive::tag<LRUListTag>>;
|
|
|
|
using AdjacencyListHook = boost::intrusive::list_base_hook<boost::intrusive::tag<AdjacencyListTag>>;
|
|
|
|
using SizeMultimapHook = boost::intrusive::set_base_hook<boost::intrusive::tag<SizeMultimapTag>>;
|
|
|
|
using KeyMapHook = boost::intrusive::set_base_hook<boost::intrusive::tag<KeyMapTag>>;
|
|
|
|
|
|
|
|
struct RegionMetadata : public LRUListHook, AdjacencyListHook, SizeMultimapHook, KeyMapHook
|
|
|
|
{
|
|
|
|
Key key;
|
|
|
|
Payload payload;
|
|
|
|
|
|
|
|
void * ptr;
|
|
|
|
size_t size;
|
2017-04-09 06:29:26 +00:00
|
|
|
size_t refcount = 0;
|
2017-04-09 05:19:50 +00:00
|
|
|
void * chunk;
|
|
|
|
|
|
|
|
bool operator< (const RegionMetadata & other) const { return size < other.size; }
|
|
|
|
|
|
|
|
bool isFree() const { return SizeMultimapHook::is_linked(); }
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
static RegionMetadata * create()
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
return new RegionMetadata;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void destroy()
|
|
|
|
{
|
|
|
|
delete this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
RegionMetadata() = default;
|
|
|
|
~RegionMetadata() = default;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct RegionCompareBySize
|
|
|
|
{
|
|
|
|
bool operator() (const RegionMetadata & a, const RegionMetadata & b) const { return a.size < b.size; }
|
|
|
|
bool operator() (const RegionMetadata & a, size_t size) const { return a.size < size; }
|
2017-04-09 06:39:26 +00:00
|
|
|
bool operator() (size_t size, const RegionMetadata & b) const { return size < b.size; }
|
2017-04-09 05:19:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
struct RegionCompareByKey
|
|
|
|
{
|
|
|
|
bool operator() (const RegionMetadata & a, const RegionMetadata & b) const { return a.key < b.key; }
|
2017-04-09 06:29:26 +00:00
|
|
|
bool operator() (const RegionMetadata & a, Key key) const { return a.key < key; }
|
2017-04-09 06:39:26 +00:00
|
|
|
bool operator() (Key key, const RegionMetadata & b) const { return key < b.key; }
|
2017-04-09 05:19:50 +00:00
|
|
|
};
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
using LRUList = boost::intrusive::list<RegionMetadata,
|
|
|
|
boost::intrusive::base_hook<LRUListHook>, boost::intrusive::constant_time_size<true>>;
|
|
|
|
using AdjacencyList = boost::intrusive::list<RegionMetadata,
|
|
|
|
boost::intrusive::base_hook<AdjacencyListHook>, boost::intrusive::constant_time_size<true>>;
|
2017-04-09 06:29:26 +00:00
|
|
|
using SizeMultimap = boost::intrusive::multiset<RegionMetadata,
|
2017-04-09 23:10:05 +00:00
|
|
|
boost::intrusive::compare<RegionCompareBySize>, boost::intrusive::base_hook<SizeMultimapHook>, boost::intrusive::constant_time_size<true>>;
|
2017-04-09 06:29:26 +00:00
|
|
|
using KeyMap = boost::intrusive::set<RegionMetadata,
|
2017-04-10 00:24:58 +00:00
|
|
|
boost::intrusive::compare<RegionCompareByKey>, boost::intrusive::base_hook<KeyMapHook>, boost::intrusive::constant_time_size<true>>;
|
2017-04-09 23:10:05 +00:00
|
|
|
|
|
|
|
/** Each region could be:
|
|
|
|
* - free: not holding any data;
|
|
|
|
* - allocated: having data, addressed by key;
|
|
|
|
* -- allocated, in use: holded externally, could not be evicted;
|
|
|
|
* -- allocated, not in use: not holded, could be evicted.
|
|
|
|
*/
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
/** Invariants:
|
|
|
|
* adjacency_list contains all regions
|
|
|
|
* size_multimap contains free regions
|
|
|
|
* key_map contains allocated regions
|
|
|
|
* lru_list contains allocated regions, that are not in use
|
|
|
|
*/
|
|
|
|
|
|
|
|
LRUList lru_list;
|
|
|
|
AdjacencyList adjacency_list;
|
|
|
|
SizeMultimap size_multimap;
|
|
|
|
KeyMap key_map;
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
mutable std::mutex mutex;
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-11 19:39:43 +00:00
|
|
|
std::mt19937_64 rng {static_cast<std::mt19937_64::result_type>(randomSeed())};
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
struct Chunk : private boost::noncopyable
|
|
|
|
{
|
|
|
|
void * ptr;
|
|
|
|
size_t size;
|
|
|
|
|
2017-04-09 07:02:13 +00:00
|
|
|
Chunk(size_t size_, void * address_hint) : size(size_)
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
2017-04-09 07:02:13 +00:00
|
|
|
ptr = mmap(address_hint, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
2017-04-09 05:19:50 +00:00
|
|
|
if (MAP_FAILED == ptr)
|
|
|
|
DB::throwFromErrno("Allocator: Cannot mmap.", DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY);
|
|
|
|
}
|
|
|
|
|
|
|
|
~Chunk()
|
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
if (ptr && 0 != munmap(ptr, size))
|
2017-04-09 05:19:50 +00:00
|
|
|
DB::throwFromErrno("Allocator: Cannot munmap.", DB::ErrorCodes::CANNOT_MUNMAP);
|
|
|
|
}
|
2017-04-09 23:10:05 +00:00
|
|
|
|
|
|
|
Chunk(Chunk && other) : ptr(other.ptr), size(other.size)
|
|
|
|
{
|
|
|
|
other.ptr = nullptr;
|
|
|
|
}
|
2017-04-09 05:19:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
using Chunks = std::list<Chunk>;
|
|
|
|
Chunks chunks;
|
|
|
|
|
|
|
|
size_t total_chunks_size = 0;
|
|
|
|
size_t total_allocated_size = 0;
|
2017-04-09 06:29:26 +00:00
|
|
|
std::atomic<size_t> total_size_currently_initialized {0};
|
2017-04-09 05:19:50 +00:00
|
|
|
size_t total_size_in_use = 0;
|
|
|
|
|
|
|
|
/// Max size of cache.
|
|
|
|
const size_t max_total_size;
|
|
|
|
|
|
|
|
/// We will allocate memory in chunks of at least that size.
|
|
|
|
/// 64 MB makes mmap overhead comparable to memory throughput.
|
|
|
|
static constexpr size_t min_chunk_size = 64 * 1024 * 1024;
|
|
|
|
|
|
|
|
/// Cache stats.
|
2017-04-09 23:10:05 +00:00
|
|
|
std::atomic<size_t> hits {0}; /// Value was in cache.
|
|
|
|
std::atomic<size_t> concurrent_hits {0}; /// Value was calculated by another thread and we was waiting for it. Also summed in hits.
|
|
|
|
std::atomic<size_t> misses {0};
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-10 00:24:58 +00:00
|
|
|
/// For whole lifetime.
|
|
|
|
size_t allocations = 0;
|
|
|
|
size_t allocated_bytes = 0;
|
|
|
|
size_t evictions = 0;
|
|
|
|
size_t evicted_bytes = 0;
|
2017-04-11 02:52:22 +00:00
|
|
|
size_t secondary_evictions = 0;
|
2017-04-10 00:24:58 +00:00
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-09 06:39:26 +00:00
|
|
|
public:
|
2017-04-09 05:19:50 +00:00
|
|
|
/// Holds region as in use. Regions in use could not be evicted from cache.
|
2017-04-09 23:10:05 +00:00
|
|
|
/// In constructor, increases refcount and if it becomes non-zero, remove region from lru_list.
|
2017-04-09 05:19:50 +00:00
|
|
|
/// In destructor, decreases refcount and if it becomes zero, insert region to lru_list.
|
|
|
|
struct Holder : private boost::noncopyable
|
|
|
|
{
|
|
|
|
Holder(ArrayCache & cache_, RegionMetadata & region_) : cache(cache_), region(region_)
|
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
if (++region.refcount == 1 && region.LRUListHook::is_linked())
|
|
|
|
cache.lru_list.erase(cache.lru_list.iterator_to(region));
|
2017-04-09 06:29:26 +00:00
|
|
|
cache.total_size_in_use += region.size;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
~Holder()
|
|
|
|
{
|
2017-04-09 06:29:26 +00:00
|
|
|
std::lock_guard<std::mutex> cache_lock(cache.mutex);
|
2017-04-09 05:19:50 +00:00
|
|
|
if (--region.refcount == 0)
|
|
|
|
cache.lru_list.push_back(region);
|
2017-04-09 06:29:26 +00:00
|
|
|
cache.total_size_in_use -= region.size;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
void * ptr() { return region.ptr; }
|
|
|
|
const void * ptr() const { return region.ptr; }
|
|
|
|
size_t size() const { return region.size; }
|
|
|
|
Key key() const { return region.key; }
|
|
|
|
Payload & payload() { return region.payload; }
|
|
|
|
const Payload & payload() const { return region.payload; }
|
|
|
|
|
|
|
|
private:
|
2017-04-09 05:19:50 +00:00
|
|
|
ArrayCache & cache;
|
|
|
|
RegionMetadata & region;
|
|
|
|
};
|
|
|
|
|
|
|
|
using HolderPtr = std::shared_ptr<Holder>;
|
2017-04-09 06:39:26 +00:00
|
|
|
private:
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
/// Represents pending insertion attempt.
|
|
|
|
struct InsertToken
|
|
|
|
{
|
|
|
|
InsertToken(ArrayCache & cache_) : cache(cache_) {}
|
|
|
|
|
|
|
|
std::mutex mutex;
|
|
|
|
bool cleaned_up = false; /// Protected by the token mutex
|
|
|
|
HolderPtr value; /// Protected by the token mutex
|
|
|
|
|
|
|
|
ArrayCache & cache;
|
|
|
|
size_t refcount = 0; /// Protected by the cache mutex
|
|
|
|
};
|
|
|
|
|
|
|
|
using InsertTokens = std::unordered_map<Key, std::shared_ptr<InsertToken>>;
|
|
|
|
InsertTokens insert_tokens;
|
|
|
|
|
|
|
|
/// This class is responsible for removing used insert tokens from the insert_tokens map.
|
|
|
|
/// Among several concurrent threads the first successful one is responsible for removal. But if they all
|
|
|
|
/// fail, then the last one is responsible.
|
|
|
|
struct InsertTokenHolder
|
|
|
|
{
|
|
|
|
const Key * key = nullptr;
|
|
|
|
std::shared_ptr<InsertToken> token;
|
|
|
|
bool cleaned_up = false;
|
|
|
|
|
|
|
|
InsertTokenHolder() = default;
|
|
|
|
|
|
|
|
void acquire(const Key * key_, const std::shared_ptr<InsertToken> & token_, std::lock_guard<std::mutex> & cache_lock)
|
|
|
|
{
|
|
|
|
key = key_;
|
|
|
|
token = token_;
|
|
|
|
++token->refcount;
|
|
|
|
}
|
|
|
|
|
|
|
|
void cleanup(std::lock_guard<std::mutex> & token_lock, std::lock_guard<std::mutex> & cache_lock)
|
|
|
|
{
|
|
|
|
token->cache.insert_tokens.erase(*key);
|
|
|
|
token->cleaned_up = true;
|
|
|
|
cleaned_up = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
~InsertTokenHolder()
|
|
|
|
{
|
|
|
|
if (!token)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (cleaned_up)
|
|
|
|
return;
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> token_lock(token->mutex);
|
|
|
|
|
|
|
|
if (token->cleaned_up)
|
|
|
|
return;
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> cache_lock(token->cache.mutex);
|
|
|
|
|
|
|
|
--token->refcount;
|
|
|
|
if (token->refcount == 0)
|
|
|
|
cleanup(token_lock, cache_lock);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
friend struct InsertTokenHolder;
|
|
|
|
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
static size_t roundUp(size_t x, size_t rounding)
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
return (x + (rounding - 1)) / rounding * rounding;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
static constexpr size_t page_size = 4096;
|
|
|
|
|
|
|
|
/// Sizes and addresses of allocated memory will be aligned to specified boundary.
|
|
|
|
static constexpr size_t alignment = 16;
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
|
|
|
|
/// Precondition: region is not in lru_list, not in key_map, not in size_multimap.
|
|
|
|
/// Postcondition: region is not in lru_list, not in key_map,
|
2017-04-10 00:24:58 +00:00
|
|
|
/// inserted into size_multimap, possibly coalesced with adjacent free regions.
|
2017-04-09 23:10:05 +00:00
|
|
|
void freeRegion(RegionMetadata & region) noexcept
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
2017-04-09 06:29:26 +00:00
|
|
|
auto adjacency_list_it = adjacency_list.iterator_to(region);
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
auto left_it = adjacency_list_it;
|
|
|
|
auto right_it = adjacency_list_it;
|
|
|
|
|
2017-04-11 02:52:22 +00:00
|
|
|
//size_t was_size = region.size;
|
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
if (left_it != adjacency_list.begin())
|
|
|
|
{
|
|
|
|
--left_it;
|
2017-04-11 02:52:22 +00:00
|
|
|
|
|
|
|
//std::cerr << "left_it->isFree(): " << left_it->isFree() << "\n";
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
if (left_it->chunk == region.chunk && left_it->isFree())
|
2017-04-10 00:24:58 +00:00
|
|
|
{
|
|
|
|
region.size += left_it->size;
|
|
|
|
*reinterpret_cast<char **>(®ion.ptr) -= left_it->size;
|
|
|
|
size_multimap.erase(size_multimap.iterator_to(*left_it));
|
|
|
|
adjacency_list.erase_and_dispose(left_it, [](RegionMetadata * elem) { elem->destroy(); });
|
|
|
|
}
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
++right_it;
|
|
|
|
if (right_it != adjacency_list.end())
|
|
|
|
{
|
2017-04-11 02:52:22 +00:00
|
|
|
//std::cerr << "right_it->isFree(): " << right_it->isFree() << "\n";
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
if (right_it->chunk == region.chunk && right_it->isFree())
|
2017-04-10 00:24:58 +00:00
|
|
|
{
|
|
|
|
region.size += right_it->size;
|
|
|
|
size_multimap.erase(size_multimap.iterator_to(*right_it));
|
|
|
|
adjacency_list.erase_and_dispose(right_it, [](RegionMetadata * elem) { elem->destroy(); });
|
|
|
|
}
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
2017-04-11 02:52:22 +00:00
|
|
|
//std::cerr << "size is enlarged: " << was_size << " -> " << region.size << "\n";
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
size_multimap.insert(region);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-11 02:52:22 +00:00
|
|
|
void evictRegion(RegionMetadata & evicted_region) noexcept
|
2017-04-09 06:29:26 +00:00
|
|
|
{
|
|
|
|
total_allocated_size -= evicted_region.size;
|
|
|
|
|
2017-04-11 02:52:22 +00:00
|
|
|
lru_list.erase(lru_list.iterator_to(evicted_region));
|
2017-04-10 00:24:58 +00:00
|
|
|
|
|
|
|
if (evicted_region.KeyMapHook::is_linked())
|
|
|
|
key_map.erase(key_map.iterator_to(evicted_region));
|
|
|
|
|
|
|
|
++evictions;
|
|
|
|
evicted_bytes += evicted_region.size;
|
2017-04-09 06:29:26 +00:00
|
|
|
|
|
|
|
freeRegion(evicted_region);
|
2017-04-11 02:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Evict region from cache and return it, coalesced with nearby free regions.
|
|
|
|
/// While size is not enough, evict adjacent regions at right, if any.
|
|
|
|
/// If nothing to evict, returns nullptr.
|
|
|
|
/// Region is removed from lru_list and key_map and inserted into size_multimap.
|
|
|
|
RegionMetadata * evictSome(size_t requested_size) noexcept
|
|
|
|
{
|
|
|
|
if (lru_list.empty())
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
/*for (const auto & elem : adjacency_list)
|
|
|
|
std::cerr << (!elem.SizeMultimapHook::is_linked() ? "\033[1m" : "") << elem.size << (!elem.SizeMultimapHook::is_linked() ? "\033[0m " : " ");
|
|
|
|
std::cerr << '\n';*/
|
|
|
|
|
|
|
|
auto it = adjacency_list.iterator_to(lru_list.front());
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
RegionMetadata & evicted_region = *it;
|
|
|
|
evictRegion(evicted_region);
|
|
|
|
|
|
|
|
if (evicted_region.size >= requested_size)
|
|
|
|
return &evicted_region;
|
|
|
|
|
|
|
|
++it;
|
|
|
|
if (it == adjacency_list.end() || it->chunk != evicted_region.chunk || !it->LRUListHook::is_linked())
|
|
|
|
return &evicted_region;
|
|
|
|
|
|
|
|
++secondary_evictions;
|
|
|
|
}
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
/// Allocates a chunk of specified size. Creates free region, spanning through whole chunk and returns it.
|
2017-04-09 23:10:05 +00:00
|
|
|
RegionMetadata * addNewChunk(size_t size)
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
2017-04-09 07:02:13 +00:00
|
|
|
/// ASLR by hand.
|
|
|
|
void * address_hint = reinterpret_cast<void *>(std::uniform_int_distribution<size_t>(0x100000000000UL, 0x700000000000UL)(rng));
|
|
|
|
|
|
|
|
chunks.emplace_back(size, address_hint);
|
2017-04-09 06:29:26 +00:00
|
|
|
Chunk & chunk = chunks.back();
|
|
|
|
|
|
|
|
total_chunks_size += size;
|
|
|
|
|
|
|
|
/// Create free region spanning through chunk.
|
2017-04-09 23:10:05 +00:00
|
|
|
RegionMetadata * free_region;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
free_region = RegionMetadata::create();
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
total_chunks_size -= size;
|
|
|
|
chunks.pop_back();
|
|
|
|
throw;
|
|
|
|
}
|
2017-04-09 06:29:26 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
free_region->ptr = chunk.ptr;
|
|
|
|
free_region->chunk = chunk.ptr;
|
|
|
|
free_region->size = chunk.size;
|
2017-04-09 06:29:26 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
adjacency_list.push_back(*free_region);
|
|
|
|
size_multimap.insert(*free_region);
|
2017-04-09 06:39:26 +00:00
|
|
|
|
|
|
|
return free_region;
|
2017-04-09 06:29:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Precondition: free_region.size >= size.
|
2017-04-09 23:10:05 +00:00
|
|
|
RegionMetadata * allocateFromFreeRegion(RegionMetadata & free_region, size_t size)
|
2017-04-09 06:29:26 +00:00
|
|
|
{
|
2017-04-10 00:24:58 +00:00
|
|
|
++allocations;
|
|
|
|
allocated_bytes += size;
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
if (free_region.size == size)
|
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
total_allocated_size += size;
|
|
|
|
size_multimap.erase(size_multimap.iterator_to(free_region));
|
|
|
|
return &free_region;
|
2017-04-09 06:29:26 +00:00
|
|
|
}
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
RegionMetadata * allocated_region = RegionMetadata::create();
|
|
|
|
total_allocated_size += size;
|
|
|
|
|
|
|
|
allocated_region->ptr = free_region.ptr;
|
2017-04-11 02:52:22 +00:00
|
|
|
allocated_region->chunk = free_region.chunk;
|
2017-04-09 23:10:05 +00:00
|
|
|
allocated_region->size = size;
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
size_multimap.erase(size_multimap.iterator_to(free_region));
|
2017-04-09 05:19:50 +00:00
|
|
|
free_region.size -= size;
|
2017-04-09 06:39:26 +00:00
|
|
|
*reinterpret_cast<char **>(&free_region.ptr) += size;
|
2017-04-09 05:19:50 +00:00
|
|
|
size_multimap.insert(free_region);
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
adjacency_list.insert(adjacency_list.iterator_to(free_region), *allocated_region);
|
2017-04-09 05:19:50 +00:00
|
|
|
return allocated_region;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
/// Does not insert allocated region to key_map or lru_list. Caller must do it.
|
2017-04-09 05:19:50 +00:00
|
|
|
RegionMetadata * allocate(size_t size)
|
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
size = roundUp(size, alignment);
|
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
/// Look up to size multimap to find free region of specified size.
|
|
|
|
auto it = size_multimap.lower_bound(size, RegionCompareBySize());
|
|
|
|
if (size_multimap.end() != it)
|
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
return allocateFromFreeRegion(*it, size);
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// If nothing was found and total size of allocated chunks plus required size is lower than maximum,
|
|
|
|
/// allocate a new chunk.
|
2017-04-09 23:10:05 +00:00
|
|
|
size_t required_chunk_size = std::max(min_chunk_size, roundUp(size, page_size));
|
2017-04-09 05:19:50 +00:00
|
|
|
if (total_chunks_size + required_chunk_size <= max_total_size)
|
|
|
|
{
|
|
|
|
/// Create free region spanning through chunk.
|
2017-04-09 23:10:05 +00:00
|
|
|
RegionMetadata * free_region = addNewChunk(required_chunk_size);
|
|
|
|
return allocateFromFreeRegion(*free_region, size);
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
2017-04-11 02:52:22 +00:00
|
|
|
// std::cerr << "Requested size: " << size << "\n";
|
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
/// Evict something from cache and continue.
|
|
|
|
while (true)
|
|
|
|
{
|
2017-04-11 02:52:22 +00:00
|
|
|
RegionMetadata * res = evictSome(size);
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
/// Nothing to evict. All cache is full and in use - cannot allocate memory.
|
|
|
|
if (!res)
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
/// Not enough. Evict more.
|
|
|
|
if (res->size < size)
|
|
|
|
continue;
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
return allocateFromFreeRegion(*res, size);
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public:
|
2017-04-09 06:29:26 +00:00
|
|
|
ArrayCache(size_t max_total_size_) : max_total_size(max_total_size_)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2017-04-09 05:19:50 +00:00
|
|
|
~ArrayCache()
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
|
2017-04-09 06:44:45 +00:00
|
|
|
key_map.clear();
|
|
|
|
lru_list.clear();
|
|
|
|
size_multimap.clear();
|
|
|
|
adjacency_list.clear_and_dispose([](RegionMetadata * elem) { elem->destroy(); });
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// If the value for the key is in the cache, returns it.
|
|
|
|
///
|
|
|
|
/// If it is not, calls 'get_size' to obtain required size of storage for key,
|
|
|
|
/// then allocates storage and call 'initialize' for necessary initialization before data from cache could be used.
|
|
|
|
///
|
|
|
|
/// Only one of several concurrent threads calling this method will call get_size or initialize,
|
|
|
|
/// others will wait for that call to complete and will use its result (this helps prevent cache stampede).
|
|
|
|
///
|
|
|
|
/// Exceptions occuring in callbacks will be propagated to the caller.
|
|
|
|
/// Another thread from the set of concurrent threads will then try to call its callbacks etc.
|
|
|
|
///
|
2017-04-09 23:10:05 +00:00
|
|
|
/// Returns cached value wrapped by holder, preventing cache entry from eviction.
|
|
|
|
/// Also could return a bool indicating whether the value was produced during this call.
|
2017-04-09 05:19:50 +00:00
|
|
|
template <typename GetSizeFunc, typename InitializeFunc>
|
2017-04-09 06:29:26 +00:00
|
|
|
HolderPtr getOrSet(const Key & key, GetSizeFunc && get_size, InitializeFunc && initialize, bool * was_calculated)
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
|
|
|
InsertTokenHolder token_holder;
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
|
2017-04-09 06:29:26 +00:00
|
|
|
auto it = key_map.find(key, RegionCompareByKey());
|
|
|
|
if (key_map.end() != it)
|
2017-04-09 05:19:50 +00:00
|
|
|
{
|
|
|
|
++hits;
|
2017-04-09 06:29:26 +00:00
|
|
|
if (was_calculated)
|
|
|
|
*was_calculated = false;
|
|
|
|
|
2017-04-09 06:39:26 +00:00
|
|
|
return std::make_shared<Holder>(*this, *it);
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
auto & token = insert_tokens[key];
|
|
|
|
if (!token)
|
|
|
|
token = std::make_shared<InsertToken>(*this);
|
|
|
|
|
|
|
|
token_holder.acquire(&key, token, cache_lock);
|
|
|
|
}
|
|
|
|
|
|
|
|
InsertToken * token = token_holder.token.get();
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> token_lock(token->mutex);
|
|
|
|
|
|
|
|
token_holder.cleaned_up = token->cleaned_up;
|
|
|
|
|
|
|
|
if (token->value)
|
|
|
|
{
|
|
|
|
/// Another thread already produced the value while we waited for token->mutex.
|
|
|
|
++hits;
|
2017-04-09 23:10:05 +00:00
|
|
|
++concurrent_hits;
|
2017-04-09 06:29:26 +00:00
|
|
|
if (was_calculated)
|
|
|
|
*was_calculated = false;
|
|
|
|
|
|
|
|
return token->value;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
++misses;
|
2017-04-09 06:29:26 +00:00
|
|
|
|
|
|
|
size_t size = get_size();
|
|
|
|
|
|
|
|
RegionMetadata * region;
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
region = allocate(size);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Cannot allocate memory.
|
|
|
|
if (!region)
|
|
|
|
return {};
|
|
|
|
|
|
|
|
region->key = key;
|
|
|
|
|
|
|
|
{
|
|
|
|
total_size_currently_initialized += size;
|
|
|
|
SCOPE_EXIT({ total_size_currently_initialized -= size; });
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
2017-04-09 06:39:26 +00:00
|
|
|
initialize(region->ptr, region->payload);
|
2017-04-09 06:29:26 +00:00
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
freeRegion(*region);
|
|
|
|
}
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
}
|
2017-04-09 05:19:50 +00:00
|
|
|
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
try
|
2017-04-09 06:29:26 +00:00
|
|
|
{
|
2017-04-09 23:10:05 +00:00
|
|
|
token->value = std::make_shared<Holder>(*this, *region);
|
|
|
|
|
|
|
|
/// Insert the new value only if the token is still in present in insert_tokens.
|
|
|
|
/// (The token may be absent because of a concurrent reset() call).
|
|
|
|
auto token_it = insert_tokens.find(key);
|
|
|
|
if (token_it != insert_tokens.end() && token_it->second.get() == token)
|
|
|
|
{
|
|
|
|
key_map.insert(*region);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!token->cleaned_up)
|
|
|
|
token_holder.cleanup(token_lock, cache_lock);
|
|
|
|
|
|
|
|
if (was_calculated)
|
|
|
|
*was_calculated = true;
|
|
|
|
|
|
|
|
return token->value;
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
if (region->KeyMapHook::is_linked())
|
|
|
|
key_map.erase(key_map.iterator_to(*region));
|
|
|
|
|
|
|
|
freeRegion(*region);
|
|
|
|
throw;
|
2017-04-09 06:29:26 +00:00
|
|
|
}
|
2017-04-09 23:10:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct Statistics
|
|
|
|
{
|
|
|
|
size_t total_chunks_size = 0;
|
|
|
|
size_t total_allocated_size = 0;
|
|
|
|
size_t total_size_currently_initialized = 0;
|
|
|
|
size_t total_size_in_use = 0;
|
|
|
|
|
|
|
|
size_t num_chunks = 0;
|
|
|
|
size_t num_regions = 0;
|
|
|
|
size_t num_free_regions = 0;
|
|
|
|
size_t num_regions_in_use = 0;
|
|
|
|
size_t num_keyed_regions = 0;
|
|
|
|
|
|
|
|
size_t hits = 0;
|
|
|
|
size_t concurrent_hits = 0;
|
|
|
|
size_t misses = 0;
|
2017-04-10 00:24:58 +00:00
|
|
|
|
|
|
|
size_t allocations = 0;
|
|
|
|
size_t allocated_bytes = 0;
|
|
|
|
size_t evictions = 0;
|
|
|
|
size_t evicted_bytes = 0;
|
2017-04-11 02:52:22 +00:00
|
|
|
size_t secondary_evictions = 0;
|
2017-04-09 23:10:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Statistics getStatistics() const
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> cache_lock(mutex);
|
|
|
|
Statistics res;
|
|
|
|
|
|
|
|
res.total_chunks_size = total_chunks_size;
|
|
|
|
res.total_allocated_size = total_allocated_size;
|
|
|
|
res.total_size_currently_initialized = total_size_currently_initialized.load(std::memory_order_relaxed);
|
|
|
|
res.total_size_in_use = total_size_in_use;
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
res.num_chunks = chunks.size();
|
|
|
|
res.num_regions = adjacency_list.size();
|
|
|
|
res.num_free_regions = size_multimap.size();
|
|
|
|
res.num_regions_in_use = adjacency_list.size() - size_multimap.size() - lru_list.size();
|
|
|
|
res.num_keyed_regions = key_map.size();
|
2017-04-09 05:19:50 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
res.hits = hits.load(std::memory_order_relaxed);
|
|
|
|
res.concurrent_hits = concurrent_hits.load(std::memory_order_relaxed);
|
|
|
|
res.misses = misses.load(std::memory_order_relaxed);
|
2017-04-09 06:29:26 +00:00
|
|
|
|
2017-04-10 00:24:58 +00:00
|
|
|
res.allocations = allocations;
|
|
|
|
res.allocated_bytes = allocated_bytes;
|
|
|
|
res.evictions = evictions;
|
|
|
|
res.evicted_bytes = evicted_bytes;
|
2017-04-11 02:52:22 +00:00
|
|
|
res.secondary_evictions = secondary_evictions;
|
2017-04-10 00:24:58 +00:00
|
|
|
|
2017-04-09 23:10:05 +00:00
|
|
|
return res;
|
2017-04-09 05:19:50 +00:00
|
|
|
}
|
|
|
|
};
|
2017-04-11 19:39:43 +00:00
|
|
|
|
|
|
|
template <typename Key, typename Payload> constexpr size_t ArrayCache<Key, Payload>::min_chunk_size;
|