Skip to content

Commit f8e560d

Browse files
committed
Initial Commit
0 parents  commit f8e560d

File tree

10 files changed

+623
-0
lines changed

10 files changed

+623
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
googletest/

.vscode/settings.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
3+
"files.associations": {
4+
"list": "cpp",
5+
"any": "cpp",
6+
"array": "cpp",
7+
"atomic": "cpp",
8+
"*.tcc": "cpp",
9+
"cctype": "cpp",
10+
"chrono": "cpp",
11+
"clocale": "cpp",
12+
"cmath": "cpp",
13+
"cstdarg": "cpp",
14+
"cstddef": "cpp",
15+
"cstdint": "cpp",
16+
"cstdio": "cpp",
17+
"cstdlib": "cpp",
18+
"ctime": "cpp",
19+
"cwchar": "cpp",
20+
"cwctype": "cpp",
21+
"deque": "cpp",
22+
"forward_list": "cpp",
23+
"unordered_map": "cpp",
24+
"unordered_set": "cpp",
25+
"vector": "cpp",
26+
"exception": "cpp",
27+
"algorithm": "cpp",
28+
"functional": "cpp",
29+
"iterator": "cpp",
30+
"map": "cpp",
31+
"memory": "cpp",
32+
"memory_resource": "cpp",
33+
"numeric": "cpp",
34+
"optional": "cpp",
35+
"random": "cpp",
36+
"ratio": "cpp",
37+
"set": "cpp",
38+
"string": "cpp",
39+
"string_view": "cpp",
40+
"system_error": "cpp",
41+
"tuple": "cpp",
42+
"type_traits": "cpp",
43+
"utility": "cpp",
44+
"fstream": "cpp",
45+
"initializer_list": "cpp",
46+
"iomanip": "cpp",
47+
"iosfwd": "cpp",
48+
"iostream": "cpp",
49+
"istream": "cpp",
50+
"limits": "cpp",
51+
"new": "cpp",
52+
"ostream": "cpp",
53+
"sstream": "cpp",
54+
"stdexcept": "cpp",
55+
"streambuf": "cpp",
56+
"cinttypes": "cpp",
57+
"typeinfo": "cpp",
58+
"variant": "cpp",
59+
"bit": "cpp",
60+
"cstring": "cpp",
61+
"bitset": "cpp",
62+
"complex": "cpp",
63+
"typeindex": "cpp"
64+
},
65+
"cmake.configureOnOpen": true
66+
}

CMakeLists.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
3+
set(This ExpiringLRUCache)
4+
5+
project(${This} C CXX)
6+
7+
set(CMAKE_C_STANDARD 11)
8+
set(CMAKE_CXX_STANDARD 17)
9+
10+
add_subdirectory(googletest)
11+
12+
enable_testing()
13+
14+
set(Headers
15+
ExpiringLRUCache.hpp
16+
)
17+
18+
add_library(${This} STATIC ${Headers})
19+
20+
add_subdirectory(examples)
21+
add_subdirectory(tests)

ExpiringLRUCache.hpp

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#ifndef EXPIRING_LRU_CACHE_HPP
2+
#define EXPIRING_LRU_CACHE_HPP 1
3+
4+
#include <list>
5+
#include <unordered_map>
6+
#include <chrono>
7+
8+
/**
9+
* @brief A container built on top of unordered_map and list to implement a LRU cache.
10+
* After a certain time, the elements of the cache expire and are lazily evicted from
11+
* the cache.
12+
*
13+
* @tparam K Type of key objects.
14+
* @tparam V Type of mapped values.
15+
*/
16+
template <class K, class V>
17+
class ExpiringLruCache
18+
{
19+
using clock_t = std::chrono::steady_clock;
20+
using time_point_t = clock_t::time_point;
21+
22+
using listk_t = typename std::list<K>;
23+
using list_it_t = typename listk_t::iterator;
24+
using tuple_t = typename std::tuple<V, list_it_t, time_point_t>;
25+
using mapkpairkv_t = typename std::unordered_map<K, typename std::tuple<V, list_it_t, time_point_t>>;
26+
27+
public:
28+
29+
using iterator = typename mapkpairkv_t::iterator;
30+
using const_iterator = typename mapkpairkv_t::const_iterator;
31+
32+
/**
33+
* Build a cache with a given capacity and TTL (time-to-live).
34+
* @param capacity The capacity of the cache. When the cache size reaches capicity,
35+
* and a new element is inserted, the least used element will be evicted from the
36+
* cache.
37+
* @param ttl Time-to-live for the elements in the cache, in seconds.
38+
*/
39+
ExpiringLruCache(size_t capacity, unsigned int ttl) : m_capacity(capacity) {
40+
m_ttl = std::chrono::seconds(ttl);
41+
}
42+
43+
/**
44+
* Get an iterator, where the first element points to a key and the second element
45+
* points to a tuple. The tuple is composed of three elements:
46+
* 0. The value associated to the key.
47+
* 1. The underlying list iterator.
48+
* 2. The time point which represents the time the element was created.
49+
*/
50+
iterator find(const K &key)
51+
{
52+
using std::chrono::duration_cast;
53+
using std::chrono::seconds;
54+
55+
const auto it = m_cache.find(key);
56+
if (it != m_cache.end())
57+
{
58+
if (duration_cast<seconds>(clock_t::now() - std::get<2>(it->second)).count() > m_ttl.count())
59+
{
60+
m_used.erase(std::get<1>(it->second));
61+
m_cache.erase(it);
62+
return m_cache.end();
63+
}
64+
65+
touch(it);
66+
}
67+
return it;
68+
}
69+
70+
/**
71+
* Returns a reference to the mapped value of the element with key k in the LRU
72+
* cache.
73+
*
74+
* If k does not match the key of any element in the container, the function throws
75+
* an out_of_range exception.
76+
*/
77+
V& at(const K &key)
78+
{
79+
const auto it = find(key);
80+
if (it == m_cache.end())
81+
{
82+
throw std::out_of_range("Out of range");
83+
}
84+
85+
return std::get<0>(it->second);
86+
}
87+
88+
/**
89+
* Returns an iterator pointing to the past-the-end element in the container.
90+
*/
91+
const_iterator end() const
92+
{
93+
return m_cache.end();
94+
}
95+
96+
/**
97+
* Insert a new elements in the LRU cache. Unlike the unordered_map emplace(), if
98+
* the key already exists, the value will be replaced with the provided value.
99+
*/
100+
void emplace(const K &key, const V &value)
101+
{
102+
const auto it = m_cache.find(key);
103+
if (it != m_cache.end())
104+
{
105+
touch(it);
106+
}
107+
else
108+
{
109+
if (m_cache.size() == m_capacity)
110+
{
111+
m_cache.erase(m_used.back());
112+
m_used.pop_back();
113+
}
114+
m_used.push_front(key);
115+
}
116+
m_cache[key] = {value, m_used.begin(), clock_t::now()};
117+
}
118+
119+
/**
120+
* Returns the number of elements in the LRU cache container.
121+
*/
122+
size_t size()
123+
{
124+
return m_cache.size();
125+
}
126+
127+
/**
128+
* Size of the underlying list, use for testing purposes only.
129+
*/
130+
size_t _listSize()
131+
{
132+
return m_used.size();
133+
}
134+
135+
private:
136+
void touch(const iterator &it)
137+
{
138+
// We use splice instead of erase()/push_front() to avoid an unnecessary copy.
139+
m_used.splice(m_used.begin(), m_used, std::get<1>(it->second));
140+
std::get<1>(it->second) = m_used.begin();
141+
std::get<2>(it->second) = clock_t::now();
142+
}
143+
144+
mapkpairkv_t m_cache;
145+
listk_t m_used;
146+
size_t m_capacity;
147+
std::chrono::duration<int64_t> m_ttl;
148+
};
149+
150+
#endif /* EXPIRING_LRU_CACHE_HPP */

LICENSE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright [2020-present] [Guillaume Jean-Philippe Humbert]
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# High Performance LRU Cache With Expiring Elements
2+
3+
This is very similar to a traditional LRU cache, with the additional feature that
4+
elements expire after a certain determined time.
5+
6+
If we try to get an element which has passed its time-to-live (TTL), it won't be
7+
considered found and will be evicted from the cache.
8+
9+
# Sample Usage
10+
11+
See the examples folder:
12+
13+
```cpp
14+
#include <iostream>
15+
#include <string>
16+
#include <unistd.h>
17+
#include "../ExpiringLRUCache.hpp"
18+
19+
int main()
20+
{
21+
using Cache = ExpiringLruCache<int, std::string>;
22+
23+
size_t capacity = 2;
24+
unsigned int timeToLiveInSeconds = 3;
25+
26+
Cache cache(capacity, timeToLiveInSeconds);
27+
28+
cache.emplace(1, "a");
29+
cache.emplace(2, "b");
30+
31+
std::cout << cache.at(1) << std::endl; // prints "a"
32+
std::cout << cache.at(2) << std::endl; // prints "b"
33+
34+
// The find() method returns an iterator, on which the first element is the key and
35+
// the second element is a tuple of three elements:
36+
// 0. The value
37+
// 1. A list iterator on the keys
38+
// 2. A chrono time point which represents the time when the element was created or
39+
// last accessed.
40+
std::cout << std::get<0>(cache.find(1)->second) << std::endl; // prints "a"
41+
std::cout << std::get<0>(cache.find(2)->second) << std::endl; // prints "b"
42+
43+
sleep(2);
44+
// Refresh the timestamp.
45+
cache.at(1);
46+
47+
sleep(2);
48+
std::cout << cache.at(1) << std::endl; // prints "a"
49+
// prints 1 (true), as the element was evicted due to being outdated
50+
std::cout << (cache.find(2) == cache.end()) << std::endl;
51+
52+
std::unordered_map<int, int> map;
53+
map.emplace(1, 10);
54+
map.emplace(1, 11);
55+
std::cout << map.at(1) << std::endl;
56+
57+
return 0;
58+
}
59+
```
60+
61+
# Build the example
62+
63+
```bash
64+
$ mkdir build
65+
$ cd build
66+
$ cmake ..
67+
$ cmake --build . --target Example
68+
```
69+
70+
To run it:
71+
72+
```bash
73+
$ ./examples/Example
74+
```
75+
76+
# Build the tests
77+
78+
```bash
79+
$ mkdir build
80+
$ cd build
81+
$ cmake ..
82+
$ cmake --build . --target ExpiringLRUCacheTests
83+
```
84+
85+
Run the tests:
86+
87+
```
88+
$ ctest -T test --output-on-failure
89+
```

examples/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
3+
set(This Example)
4+
5+
set(Sources
6+
Example.cpp
7+
)
8+
9+
# Needed as the library provides only a ".hpp" file which is not automatically
10+
# recognized by cmake.
11+
set_target_properties(ExpiringLRUCache PROPERTIES LINKER_LANGUAGE CXX)
12+
13+
add_executable(${This} ${Sources})
14+
target_link_libraries(${This} PUBLIC
15+
ExpiringLRUCache
16+
)

0 commit comments

Comments
 (0)