/////////////////////////////////////////////////////////////////////////////// // Copyright (c) Electronic Arts Inc. All rights reserved. /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // lru_cache is a container that simplifies caching of objects in a map. // Basically, you give the container a key, like a string, and the data you want. // The container provides callback mechanisms to generate data if it's missing // as well as delete data when it's purged from the cache. This container // uses a least recently used method: whatever the oldest item is will be // replaced with a new entry. // // Algorithmically, the container is a combination of a map and a list. // The list stores the age of the entries by moving the entry to the head // of the list on each access, either by a call to get() or to touch(). // The map is just the map as one would expect. // // This is useful for caching off data that is expensive to generate, // for example text to speech wave files that are dynamically generated, // but that will need to be reused, as is the case in narration of menu // entries as a user scrolls through the entries. /////////////////////////////////////////////////////////////////////////////// #ifndef EASTL_LRUCACHE_H #define EASTL_LRUCACHE_H #if defined(EA_PRAGMA_ONCE_SUPPORTED) #pragma once #endif #include #include #include namespace eastl { /// EASTL_LRUCACHE_DEFAULT_NAME /// /// Defines a default container name in the absence of a user-provided name. /// #ifndef EASTL_LRUCACHE_DEFAULT_NAME #define EASTL_LRUCACHE_DEFAULT_NAME EASTL_DEFAULT_NAME_PREFIX " lru_cache" // Unless the user overrides something, this is "EASTL lru_cache". #endif /// EASTL_LRUCACHE_DEFAULT_ALLOCATOR /// #ifndef EASTL_LRUCACHE_DEFAULT_ALLOCATOR #define EASTL_LRUCACHE_DEFAULT_ALLOCATOR allocator_type(EASTL_LRUCACHE_DEFAULT_NAME) #endif /// lru_cache /// /// Implements a caching map based off of a key and data. /// LRUList parameter is any container that guarantees the validity of its iterator even after a modification (e.g. list) /// LRUMap is any mapping container that can map a key to some data. By default, we use unordered_set, but it might be better /// to use hash_map or some other structure depending on your key/data combination. For example, you may want to swap the /// map backing if using strings as keys or if the data objects are small. In any case, unordered_set is a good default and should /// work well enough since the purpose of this class is to cache results of expensive, order of milliseconds, operations /// /// Algorithmic Performance (default data structures): /// touch() -> O(1) /// insert() / update(), get() / operator[] -> equivalent to unordered_set (O(1) on average, O(n) worst) /// size() -> O(1) /// /// All accesses to a given key (insert, update, get) will push that key to most recently used. /// If the data objects are shared between threads, it would be best to use a smartptr to manage the lifetime of the data. /// as it could be removed from the cache while in use by another thread. template , typename map_type = eastl::unordered_map, eastl::hash, eastl::equal_to, Allocator>> class lru_cache { public: using key_type = Key; using value_type = Value; using allocator_type = Allocator; using size_type = eastl_size_t; using list_iterator = typename list_type::iterator; using map_iterator = typename map_type::iterator; using data_container_type = eastl::pair; using iterator = typename map_type::iterator; using const_iterator = typename map_type::const_iterator; using this_type = lru_cache; using create_callback_type = eastl::function; using delete_callback_type = eastl::function; /// lru_cache constructor /// /// Creates a Key / Value map that only stores size Value objects until it deletes them. /// For complex objects or operations, the creator and deletor callbacks can be used. /// This works just like a regular map object: on access, the Value will be created if it doesn't exist, returned otherwise. explicit lru_cache(size_type size, const allocator_type& allocator = EASTL_LRUCACHE_DEFAULT_ALLOCATOR, create_callback_type creator = nullptr, delete_callback_type deletor = nullptr) : m_list(allocator) , m_map(allocator) , m_capacity(size) , m_create_callback(creator) , m_delete_callback(deletor) { } /// lru_cache destructor /// /// Iterates across every entry in the map and calls the deletor before calling the standard destructors ~lru_cache() { // Destruct everything we have cached for (auto& iter : m_map) { if (m_delete_callback) m_delete_callback(iter.second.first); } } lru_cache(std::initializer_list> il) : lru_cache(il.size()) { for(auto& p : il) insert_or_assign(p.first, p.second); } // TODO(rparolin): Why do we prevent copies? And what about moves? lru_cache(const this_type&) = delete; this_type &operator=(const this_type&) = delete; /// insert /// /// insert key k with value v. /// If key already exists, no change is made and the return value is false. /// If the key doesn't exist, the data is added to the map and the return value is true. bool insert(const key_type& k, const value_type& v) { if (m_map.find(k) == m_map.end()) { make_space(); m_list.push_front(k); m_map[k] = data_container_type(v, m_list.begin()); return true; } else { return false; } } /// emplace /// /// Places a new object in place k created with args /// If the key already exists, it is replaced. template void emplace(const key_type& k, Args&&... args) { make_space(); m_list.push_front(k); m_map.emplace(k, data_container_type(eastl::forward(args)..., m_list.begin())); } /// insert_or_assign /// /// Same as add, but replaces the data at key k, if it exists, with the new entry v /// Note that the deletor for the old v will be called before it's replaced with the new value of v void insert_or_assign(const key_type& k, const value_type& v) { auto iter = m_map.find(k); if (m_map.find(k) != m_map.end()) { assign(iter, v); } else { insert(k, v); } } /// contains /// /// Returns true if key k exists in the cache bool contains(const key_type& k) const { return m_map.find(k) != m_map.end(); } /// at /// /// Retrives the data for key k, not valid if k does not exist eastl::optional at(const key_type& k) { auto iter = m_map.find(k); if (iter != m_map.end()) { return iter->second.first; } else { return eastl::nullopt; } } /// get /// /// Retrives the data for key k. If no data exists, it will be created by calling the /// creator. value_type& get(const key_type& k) { auto iter = m_map.find(k); // The entry exists in the cache if (iter != m_map.end()) { touch(k); return iter->second.first; } else // The entry doesn't exist in the cache, so create one { // Add the entry to the map insert(k, m_create_callback ? m_create_callback(k) : value_type()); // return the new data return m_map[k].first; } } /// Equivalent to get(k) value_type& operator[](const key_type& k) { return get(k); } /// erase /// /// erases key k from the cache. /// If k does not exist, returns false. If k exists, returns true. bool erase(const key_type& k) { auto iter = m_map.find(k); if (iter != m_map.end()) { m_list.erase(iter->second.second); // Delete the actual entry map_erase(iter); return true; } return false; } /// erase_oldest /// /// Removes the oldest entry from the cache. void erase_oldest() { auto key = m_list.back(); m_list.pop_back(); // Delete the actual entry auto iter = m_map.find(key); map_erase(iter); } /// touch /// /// Touches key k, marking it as most recently used. /// If k does not exist, returns false. If the touch was successful, returns true. bool touch(const key_type& k) { auto iter = m_map.find(k); if (iter != m_map.end()) { touch(iter); return true; } return false; } /// touch /// /// Touches key at iterator iter, moving it to most recently used position void touch(iterator& iter) { auto listRef = iter->second.second; m_list.erase(listRef); m_list.push_front(iter->first); iter->second.second = m_list.begin(); } /// assign /// /// Updates key k with data v. /// If key k does not exist, returns false and no changes are made. /// If key k exists, existing data has its deletor called and key k's data is replaced with new v data bool assign(const key_type& k, const value_type& v) { auto iter = m_map.find(k); if (iter != m_map.end()) { assign(iter, v); return true; } return false; } /// assign /// /// Updates data at spot iter with data v. void assign(iterator& iter, const value_type& v) { if (m_delete_callback) m_delete_callback(iter->second.first); touch(iter); iter->second.first = v; } // standard container functions iterator begin() EA_NOEXCEPT { return m_map.begin(); } iterator end() EA_NOEXCEPT { return m_map.end(); } iterator rbegin() EA_NOEXCEPT { return m_map.rbegin(); } iterator rend() EA_NOEXCEPT { return m_map.rend(); } const_iterator begin() const EA_NOEXCEPT { return m_map.begin(); } const_iterator cbegin() const EA_NOEXCEPT { return m_map.cbegin(); } const_iterator crbegin() const EA_NOEXCEPT { return m_map.crbegin(); } const_iterator end() const EA_NOEXCEPT { return m_map.end(); } const_iterator cend() const EA_NOEXCEPT { return m_map.cend(); } const_iterator crend() const EA_NOEXCEPT { return m_map.crend(); } bool empty() const EA_NOEXCEPT { return m_map.empty(); } size_type size() const EA_NOEXCEPT { return m_map.size(); } size_type capacity() const EA_NOEXCEPT { return m_capacity; } void clear() EA_NOEXCEPT { // Since we have a delete callback, we want to reuse the trim function by cheating the max // size to clear all the entries to avoid duplicating code. auto old_max = m_capacity; m_capacity = 0; trim(); m_capacity = old_max; } /// resize /// /// Resizes the cache. Can be used to either expand or contract the cache. /// In the case of a contraction, the oldest entries will be evicted with their respective /// deletors called before completing. void resize(size_type newSize) { m_capacity = newSize; trim(); } void setCreateCallback(create_callback_type callback) { m_create_callback = callback; } void setDeleteCallback(delete_callback_type callback) { m_delete_callback = callback; } // EASTL extensions const allocator_type& get_allocator() const EA_NOEXCEPT { return m_map.get_allocator(); } allocator_type& get_allocator() EA_NOEXCEPT { return m_map.get_allocator(); } void set_allocator(const allocator_type& allocator) { m_map.set_allocator(allocator); m_list.set_allocator(allocator); } /// Does not reset the callbacks void reset_lose_memory() EA_NOEXCEPT { m_map.reset_lose_memory(); m_list.reset_lose_memory(); } private: inline void map_erase(map_iterator pos) { if (m_delete_callback) m_delete_callback(pos->second.first); m_map.erase(pos); } bool trim() { if (size() <= m_capacity) { return false; // No trim necessary } // We need to trim do { erase_oldest(); } while (m_list.size() > m_capacity); return true; } void make_space() { if (size() == m_capacity) { erase_oldest(); } } private: list_type m_list; map_type m_map; size_type m_capacity; create_callback_type m_create_callback; delete_callback_type m_delete_callback; }; } #endif