From c37441bf507cb98f916f447558613c949e217af3 Mon Sep 17 00:00:00 2001 From: Vladimir Byko-Ianko Date: Thu, 29 Mar 2018 14:40:52 +0300 Subject: [PATCH] LRU cache implementation. --- base/CMakeLists.txt | 1 + base/lru_cache.hpp | 164 ++++++++++++++++++++++ xcode/base/base.xcodeproj/project.pbxproj | 4 + 3 files changed, 169 insertions(+) create mode 100644 base/lru_cache.hpp diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 53856cdc92..cd78655469 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -42,6 +42,7 @@ set( logging.cpp logging.hpp lower_case.cpp + lru_cache.hpp macros.hpp math.hpp matrix.hpp diff --git a/base/lru_cache.hpp b/base/lru_cache.hpp new file mode 100644 index 0000000000..62b3ad6412 --- /dev/null +++ b/base/lru_cache.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include "base/assert.hpp" + +#include +#include +#include +#include + +/// \brief Implementation of cache with least recently used replacement policy. +template +class LruCache +{ + template friend class LruCacheTest; + template friend class LruCacheKeyAgeTest; +public: + using Loader = std::function; + + /// \param maxCacheSize Maximum size of the cache in number of items. It should be one or greater. + /// \param loader Function which is called if it's necessary to load a new item for the cache. + /// For the same |key| should be loaded the same |value|. + LruCache(size_t maxCacheSize, Loader const & loader) + : m_maxCacheSize(maxCacheSize), m_loader(loader) + { + CHECK_GREATER(maxCacheSize, 0, ()); + } + + /// \brief Loads value, if it's necessary, by |key| with |m_loader|, puts it to |m_cache| and + /// returns the reference to the value to |m_cache|. + Value const & GetValue(Key const & key) + { + auto const it = m_cache.find(key); + if (it != m_cache.cend()) + { + m_keyAge.UpdateAge(key); + return it->second; + } + + if (m_cache.size() >= m_maxCacheSize) + { + m_cache.erase(m_keyAge.GetLruKey()); + m_keyAge.RemoveLru(); + } + + m_keyAge.InsertKey(key); + Value & value = m_cache[key]; + m_loader(key, value); + return value; + } + + /// \brief Checks for coherence class params. + /// \note It's a time consumption method and should be called for tests only. + bool IsValid() const + { + if (!m_keyAge.IsValidForTesting()) + return false; + + for (auto const & kv : m_cache) + { + if (!m_keyAge.IsKeyValidForTesting(kv.first)) + return false; + } + + return true; + } + +private: + /// \brief This class support cross mapping from age to key and for key to age. + /// It lets effectively get least recently used key (key with minimum value of age) + /// and find key age by its value to update the key age. + /// \note Size of |m_ageToKey| and |m_ageToKey| should be the sameß. + /// All keys of |m_ageToKey| should be values of |m_ageToKey| and on the contrary + /// all keys of |m_ageToKey| should be values of |m_ageToKey|. + /// \note Ages should be unique for all keys. + class KeyAge + { + template friend class LruCacheKeyAgeTest; + public: + /// \brief Increments |m_age| and insert key to |m_ageToKey| and |m_keyToAge|. + /// \note This method should be used only if there's no |key| in |m_ageToKey| and |m_keyToAge|. + void InsertKey(Key const & key) + { + ++m_age; + m_ageToKey[m_age] = key; + m_keyToAge[key] = m_age; + } + + /// \brief Increments |m_age| and updates key age in |m_ageToKey| and |m_keyToAge|. + /// \note This method should be used only if there's |key| in |m_ageToKey| and |m_keyToAge|. + void UpdateAge(Key const & key) + { + ++m_age; + auto keyToAgeIt = m_keyToAge.find(key); + CHECK(keyToAgeIt != m_keyToAge.end(), ()); + // Removing former age. + size_t const removed = m_ageToKey.erase(keyToAgeIt->second); + CHECK_EQUAL(removed, 1, ()); + // Putting new age. + m_ageToKey[m_age] = key; + m_keyToAge[key] = m_age; + } + + /// \returns Least recently used key without updating the age. + /// \note |m_ageToKey| and |m_keyToAge| shouldn't be empty. + Key const & GetLruKey() const + { + CHECK(!m_ageToKey.empty(), ()); + // The smaller age the older item. + return m_ageToKey.cbegin()->second; + } + + void RemoveLru() + { + Key const & lru = GetLruKey(); + size_t const removed = m_keyToAge.erase(lru); + CHECK_EQUAL(removed, 1, ()); + m_ageToKey.erase(m_ageToKey.begin()); + } + + /// \brief Checks for coherence class params. + /// \note It's a time consumption method and should be called for tests only. + bool IsValidForTesting() const + { + for (auto const & kv : m_ageToKey) + { + if (kv.first > m_age) + return false; + if (m_keyToAge.find(kv.second) == m_keyToAge.cend()) + return false; + } + for (auto const & kv : m_keyToAge) + { + if (kv.second > m_age) + return false; + if (m_ageToKey.find(kv.second) == m_ageToKey.cend()) + return false; + } + return true; + } + + /// \returns true if |key| and its age are contained in |m_keyToAge| and |m_keyToAge|. + /// \note It's a time consumption method and should be called for tests only. + bool IsKeyValidForTesting(Key const & key) const + { + auto const keyToAgeId = m_keyToAge.find(key); + if (keyToAgeId == m_keyToAge.cend()) + return false; + auto const ageToKeyIt = m_ageToKey.find(keyToAgeId->second); + if (ageToKeyIt == m_ageToKey.cend()) + return false; + return key == ageToKeyIt->second; + } + + private: + size_t m_age = 0; + std::map m_ageToKey; + std::map m_keyToAge; + }; + + size_t const m_maxCacheSize; + Loader const m_loader; + std::unordered_map m_cache; + KeyAge m_keyAge; +}; diff --git a/xcode/base/base.xcodeproj/project.pbxproj b/xcode/base/base.xcodeproj/project.pbxproj index 3abbe141ee..bbef55390e 100644 --- a/xcode/base/base.xcodeproj/project.pbxproj +++ b/xcode/base/base.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 3D78157B1F3D89EC0068B6AC /* waiter.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3D78157A1F3D89EC0068B6AC /* waiter.hpp */; }; 3DBD7B95240FB11200ED9FE8 /* checked_cast_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD7B94240FB11200ED9FE8 /* checked_cast_tests.cpp */; }; 564BB445206E89ED00BDD211 /* fifo_cache.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 564BB444206E89ED00BDD211 /* fifo_cache.hpp */; }; + 56290B8A206D0887003892E0 /* lru_cache.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 56290B89206D0887003892E0 /* lru_cache.hpp */; }; 56B1A0741E69DE4D00395022 /* random.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 56B1A0711E69DE4D00395022 /* random.cpp */; }; 56B1A0751E69DE4D00395022 /* random.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 56B1A0721E69DE4D00395022 /* random.hpp */; }; 56B1A0761E69DE4D00395022 /* small_set.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 56B1A0731E69DE4D00395022 /* small_set.hpp */; }; @@ -234,6 +235,7 @@ 3DBD7B94240FB11200ED9FE8 /* checked_cast_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = checked_cast_tests.cpp; sourceTree = ""; }; 564BB444206E89ED00BDD211 /* fifo_cache.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = fifo_cache.hpp; sourceTree = ""; }; 564BB446206E8A4D00BDD211 /* fifo_cache_test.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = fifo_cache_test.cpp; sourceTree = ""; }; + 56290B89206D0887003892E0 /* lru_cache.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = lru_cache.hpp; sourceTree = ""; }; 56B1A0711E69DE4D00395022 /* random.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = random.cpp; sourceTree = ""; }; 56B1A0721E69DE4D00395022 /* random.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = random.hpp; sourceTree = ""; }; 56B1A0731E69DE4D00395022 /* small_set.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = small_set.hpp; sourceTree = ""; }; @@ -433,6 +435,7 @@ 3917FA56211E009700937DF4 /* get_time.hpp */, 3917FA52211E008C00937DF4 /* clustering_map.hpp */, 564BB444206E89ED00BDD211 /* fifo_cache.hpp */, + 56290B89206D0887003892E0 /* lru_cache.hpp */, 675341851A3F57E400A0A8C3 /* array_adapters.hpp */, 675341861A3F57E400A0A8C3 /* assert.hpp */, 675341871A3F57E400A0A8C3 /* base.cpp */, @@ -551,6 +554,7 @@ 675341E31A3F57E400A0A8C3 /* math.hpp in Headers */, 3446C6731DDCA96300146687 /* levenshtein_dfa.hpp in Headers */, 56B1A0751E69DE4D00395022 /* random.hpp in Headers */, + 56290B8A206D0887003892E0 /* lru_cache.hpp in Headers */, 675341E21A3F57E400A0A8C3 /* macros.hpp in Headers */, 672DD4C51E0425600078E13C /* observer_list.hpp in Headers */, 675341EF1A3F57E400A0A8C3 /* rolling_hash.hpp in Headers */,