Directory: | cvmfs/ |
---|---|
File: | cvmfs/lru.h |
Date: | 2025-06-22 02:36:02 |
Exec | Total | Coverage | |
---|---|---|---|
Lines: | 320 | 324 | 98.8% |
Branches: | 135 | 246 | 54.9% |
Line | Branch | Exec | Source |
---|---|---|---|
1 | /** | ||
2 | * This file is part of the CernVM File System. | ||
3 | * | ||
4 | * This class provides an Least Recently Used (LRU) cache for arbitrary data | ||
5 | * It stores Key-Value pairs of arbitrary data types in a hash table and | ||
6 | * automatically deletes the entries which are least touched in the last time | ||
7 | * to prevent the structure from growing beyond a given maximal cache size. | ||
8 | * The cache uses a hand crafted memory allocator to use memory efficiently | ||
9 | * | ||
10 | * Hash functions have to be provided. They should return an equal distribution | ||
11 | * of keys in uint32_t. In addition, a special key has to be provided that is | ||
12 | * used to mark "empty" elements in the hash table. | ||
13 | * | ||
14 | * The cache size has to be a multiply of 64. | ||
15 | * | ||
16 | * usage: | ||
17 | * // 100 entries, -1 special key | ||
18 | * LruCache<int, string> cache(100, -1, hasher_int); | ||
19 | * | ||
20 | * // Inserting some stuff | ||
21 | * cache.insert(42, "fourtytwo"); | ||
22 | * cache.insert(2, "small prime number"); | ||
23 | * cache.insert(1337, "leet"); | ||
24 | * | ||
25 | * // Trying to retrieve a value | ||
26 | * int result; | ||
27 | * if (cache.lookup(21, result)) { | ||
28 | * cout << "cache hit: " << result << endl; | ||
29 | * } else { | ||
30 | * cout << "cache miss" << endl; | ||
31 | * } | ||
32 | * | ||
33 | * cache.drop(); // Empty the cache | ||
34 | */ | ||
35 | |||
36 | #ifndef CVMFS_LRU_H_ | ||
37 | #define CVMFS_LRU_H_ | ||
38 | |||
39 | // If defined the cache is secured by a posix mutex | ||
40 | #define LRU_CACHE_THREAD_SAFE | ||
41 | |||
42 | #include <stdint.h> | ||
43 | |||
44 | #include <algorithm> | ||
45 | #include <cassert> | ||
46 | #include <cstring> | ||
47 | #include <functional> | ||
48 | #include <map> | ||
49 | #include <string> | ||
50 | |||
51 | #include "smallhash.h" | ||
52 | #include "statistics.h" | ||
53 | #include "util/atomic.h" | ||
54 | #include "util/platform.h" | ||
55 | #include "util/single_copy.h" | ||
56 | #include "util/smalloc.h" | ||
57 | |||
58 | namespace lru { | ||
59 | |||
60 | /** | ||
61 | * Counting of cache operations. | ||
62 | */ | ||
63 | struct Counters { | ||
64 | perf::Counter *sz_size; | ||
65 | perf::Counter *n_hit; | ||
66 | perf::Counter *n_miss; | ||
67 | perf::Counter *n_insert; | ||
68 | perf::Counter *n_insert_negative; | ||
69 | uint64_t num_collisions; | ||
70 | uint32_t max_collisions; | ||
71 | perf::Counter *n_update; | ||
72 | perf::Counter *n_update_value; | ||
73 | perf::Counter *n_replace; | ||
74 | perf::Counter *n_forget; | ||
75 | perf::Counter *n_drop; | ||
76 | perf::Counter *sz_allocated; | ||
77 | |||
78 | 5777 | explicit Counters(perf::StatisticsTemplate statistics) { | |
79 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | sz_size = statistics.RegisterTemplated("sz_size", "Total size"); |
80 | 5777 | num_collisions = 0; | |
81 | 5777 | max_collisions = 0; | |
82 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_hit = statistics.RegisterTemplated("n_hit", "Number of hits"); |
83 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_miss = statistics.RegisterTemplated("n_miss", "Number of misses"); |
84 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_insert = statistics.RegisterTemplated("n_insert", "Number of inserts"); |
85 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_insert_negative = statistics.RegisterTemplated( |
86 | "n_insert_negative", "Number of negative inserts"); | ||
87 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_update = statistics.RegisterTemplated("n_update", "Number of updates"); |
88 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_update_value = statistics.RegisterTemplated("n_update_value", |
89 | "Number of value changes"); | ||
90 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_replace = statistics.RegisterTemplated("n_replace", "Number of replaces"); |
91 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_forget = statistics.RegisterTemplated("n_forget", "Number of forgets"); |
92 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | n_drop = statistics.RegisterTemplated("n_drop", "Number of drops"); |
93 |
3/6✓ Branch 2 taken 5777 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 5777 times.
✗ Branch 10 not taken.
|
5777 | sz_allocated = statistics.RegisterTemplated("sz_allocated", |
94 | "Number of allocated bytes "); | ||
95 | 5777 | } | |
96 | }; | ||
97 | |||
98 | |||
99 | /** | ||
100 | * Template class to create a LRU cache | ||
101 | * @param Key type of the key values | ||
102 | * @param Value type of the value values | ||
103 | */ | ||
104 | template<class Key, class Value> | ||
105 | class LruCache : SingleCopy { | ||
106 | private: | ||
107 | // Forward declarations of private internal data structures | ||
108 | template<class T> | ||
109 | class ListEntry; | ||
110 | template<class T> | ||
111 | class ListEntryHead; | ||
112 | template<class T> | ||
113 | class ListEntryContent; | ||
114 | template<class M> | ||
115 | class MemoryAllocator; | ||
116 | |||
117 | // Helpers to get the template magic right | ||
118 | typedef ListEntryContent<Key> ConcreteListEntryContent; | ||
119 | typedef MemoryAllocator<ConcreteListEntryContent> ConcreteMemoryAllocator; | ||
120 | |||
121 | /** | ||
122 | * This structure wraps the user data and relates it to the LRU list entry | ||
123 | */ | ||
124 | typedef struct { | ||
125 | ListEntryContent<Key> *list_entry; | ||
126 | Value value; | ||
127 | } CacheEntry; | ||
128 | |||
129 | // static uint64_t GetEntrySize() { return sizeof(Key) + sizeof(Value); } | ||
130 | |||
131 | /** | ||
132 | * A special purpose memory allocator for the cache entries. | ||
133 | * It allocates enough memory for the maximal number of cache entries at | ||
134 | * startup, and assigns new ListEntryContent objects to a free spot in this | ||
135 | * memory pool (by overriding the 'new' and 'delete' operators of | ||
136 | * ListEntryContent). | ||
137 | * | ||
138 | * @param T the type of object to be allocated by this MemoryAllocator | ||
139 | */ | ||
140 | template<class T> | ||
141 | class MemoryAllocator : SingleCopy { | ||
142 | public: | ||
143 | /** | ||
144 | * Creates a MemoryAllocator to handle a memory pool for objects of type T | ||
145 | * @param num_slots the number of slots to be allocated for the given | ||
146 | * datatype T | ||
147 | */ | ||
148 | 8410 | explicit MemoryAllocator(const unsigned int num_slots) { | |
149 | // how many bitmap chunks (chars) do we need? | ||
150 | 8410 | const unsigned int num_bytes_bitmap = num_slots / 8; | |
151 | 8410 | bits_per_block_ = 8 * sizeof(bitmap_[0]); | |
152 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5777 times.
|
8410 | assert((num_slots % bits_per_block_) == 0); |
153 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5777 times.
|
8410 | assert(num_slots >= 2 * bits_per_block_); |
154 | |||
155 | // How much actual memory do we need? | ||
156 | 8410 | const unsigned int num_bytes_memory = sizeof(T) * num_slots; | |
157 | |||
158 | // Allocate zero'd memory | ||
159 | 8410 | bitmap_ = reinterpret_cast<uint64_t *>(scalloc(num_bytes_bitmap, 1)); | |
160 | 8410 | memory_ = reinterpret_cast<T *>(scalloc(num_bytes_memory, 1)); | |
161 | |||
162 | // Create initial state | ||
163 | 8410 | num_slots_ = num_slots; | |
164 | 8410 | num_free_slots_ = num_slots; | |
165 | 8410 | next_free_slot_ = 0; | |
166 | 8410 | bytes_allocated_ = num_bytes_bitmap + num_bytes_memory; | |
167 | 8410 | } | |
168 | |||
169 | /** | ||
170 | * Number of bytes for a single entry | ||
171 | */ | ||
172 | 4686 | static double GetEntrySize() { | |
173 | 4686 | return static_cast<double>(sizeof(T)) + 1.0 / 8.0; | |
174 | } | ||
175 | |||
176 | /** | ||
177 | * The memory allocator also frees all allocated data | ||
178 | */ | ||
179 | 11550 | virtual ~MemoryAllocator() { | |
180 | 11550 | free(bitmap_); | |
181 | 11550 | free(memory_); | |
182 | } | ||
183 | |||
184 | /** | ||
185 | * Check if the memory pool is full. | ||
186 | * @return true if all slots are occupied, otherwise false | ||
187 | */ | ||
188 | 106452 | inline bool IsFull() const { return num_free_slots_ == 0; } | |
189 | |||
190 | 53226 | T *Construct(const T object) { | |
191 | 53226 | T *mem = Allocate(); | |
192 |
1/2✓ Branch 0 taken 52406 times.
✗ Branch 1 not taken.
|
53226 | if (mem != NULL) { |
193 |
1/2✓ Branch 2 taken 52406 times.
✗ Branch 3 not taken.
|
53226 | new (static_cast<void *>(mem)) T(object); |
194 | } | ||
195 | 53226 | return mem; | |
196 | } | ||
197 | |||
198 | 53222 | void Destruct(T *object) { | |
199 | 53222 | object->~T(); | |
200 | 53222 | Deallocate(object); | |
201 | 53222 | } | |
202 | |||
203 | /** | ||
204 | * Allocate a slot and returns a pointer to the memory. | ||
205 | * @return a pointer to a chunk of the memory pool | ||
206 | */ | ||
207 | 53226 | T *Allocate() { | |
208 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 52406 times.
|
53226 | if (this->IsFull()) |
209 | ✗ | return NULL; | |
210 | |||
211 | // Allocate a slot | ||
212 | 53226 | this->SetBit(next_free_slot_); | |
213 | 53226 | --num_free_slots_; | |
214 | 53226 | T *slot = memory_ + next_free_slot_; | |
215 | |||
216 | // Find a new free slot if there are some left | ||
217 |
2/2✓ Branch 1 taken 41164 times.
✓ Branch 2 taken 11242 times.
|
53226 | if (!this->IsFull()) { |
218 | 41984 | unsigned bitmap_block = next_free_slot_ / bits_per_block_; | |
219 |
2/2✓ Branch 0 taken 536 times.
✓ Branch 1 taken 41164 times.
|
42520 | while (~bitmap_[bitmap_block] == 0) |
220 | 536 | bitmap_block = (bitmap_block + 1) % (num_slots_ / bits_per_block_); | |
221 | // TODO(jblomer): faster search inside the int | ||
222 | 41984 | next_free_slot_ = bitmap_block * bits_per_block_; | |
223 |
2/2✓ Branch 1 taken 1182384 times.
✓ Branch 2 taken 41164 times.
|
1226154 | while (this->GetBit(next_free_slot_)) |
224 | 1184170 | next_free_slot_++; | |
225 | } | ||
226 | |||
227 | 53226 | return slot; | |
228 | } | ||
229 | |||
230 | /** | ||
231 | * Free a given slot in the memory pool | ||
232 | * @param slot a pointer to the slot be freed | ||
233 | */ | ||
234 | 53222 | void Deallocate(T *slot) { | |
235 | // Check if given slot is in bounds | ||
236 |
2/4✓ Branch 0 taken 52402 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52402 times.
✗ Branch 3 not taken.
|
53222 | assert((slot >= memory_) && (slot <= memory_ + num_slots_)); |
237 | |||
238 | // Get position of slot | ||
239 | 53222 | const unsigned int position = slot - memory_; | |
240 | |||
241 | // Check if slot was already freed | ||
242 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 52402 times.
|
53222 | assert(this->GetBit(position)); |
243 | |||
244 | // Free slot, save the position of this slot as free (faster reallocation) | ||
245 | 53222 | this->UnsetBit(position); | |
246 | 53222 | next_free_slot_ = position; | |
247 | 53222 | ++num_free_slots_; | |
248 | 53222 | } | |
249 | |||
250 | 8432 | uint64_t bytes_allocated() { return bytes_allocated_; } | |
251 | |||
252 | private: | ||
253 | /** | ||
254 | * Check a bit in the internal allocation bitmap. | ||
255 | * @param position the position to check | ||
256 | * @return true if bit is set, otherwise false | ||
257 | */ | ||
258 | 1279376 | inline bool GetBit(const unsigned position) { | |
259 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1275950 times.
|
1279376 | assert(position < num_slots_); |
260 | 1279376 | return ((bitmap_[position / bits_per_block_] | |
261 | 1279376 | & (uint64_t(1) << (position % bits_per_block_))) | |
262 | 1279376 | != 0); | |
263 | } | ||
264 | |||
265 | /** | ||
266 | * set a bit in the internal allocation bitmap | ||
267 | * @param position the number of the bit to be set | ||
268 | */ | ||
269 | 53226 | inline void SetBit(const unsigned position) { | |
270 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52406 times.
|
53226 | assert(position < num_slots_); |
271 | 53226 | bitmap_[position / bits_per_block_] |= uint64_t(1) | |
272 | 53226 | << (position % bits_per_block_); | |
273 | 53226 | } | |
274 | |||
275 | /** | ||
276 | * Clear a bit in the internal allocation bitmap | ||
277 | * @param position the number of the bit to be cleared | ||
278 | */ | ||
279 | 53222 | inline void UnsetBit(const unsigned position) { | |
280 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52402 times.
|
53222 | assert(position < num_slots_); |
281 | 53222 | bitmap_[position / bits_per_block_] &= ~(uint64_t(1) | |
282 | 53222 | << (position % bits_per_block_)); | |
283 | 53222 | } | |
284 | |||
285 | unsigned int num_slots_; /**< Overall number of slots in memory pool. */ | ||
286 | unsigned int num_free_slots_; /**< Current number of free slots left. */ | ||
287 | unsigned int next_free_slot_; /**< Position of next free slot in pool. */ | ||
288 | uint64_t bytes_allocated_; | ||
289 | uint64_t *bitmap_; /**< A bitmap to mark slots as allocated. */ | ||
290 | unsigned bits_per_block_; | ||
291 | T *memory_; /**< The memory pool, array of Ms. */ | ||
292 | }; | ||
293 | |||
294 | |||
295 | /** | ||
296 | * Internal LRU list entry, to maintain the doubly linked list. | ||
297 | * The list keeps track of the least recently used keys in the cache. | ||
298 | */ | ||
299 | template<class T> | ||
300 | class ListEntry { | ||
301 | friend class LruCache; | ||
302 | |||
303 | public: | ||
304 | /** | ||
305 | * Create a new list entry as lonely, both next and prev pointing to this. | ||
306 | */ | ||
307 | 61636 | ListEntry() { | |
308 | 61636 | this->next = this; | |
309 | 61636 | this->prev = this; | |
310 | 61636 | } | |
311 | |||
312 | 53226 | ListEntry(const ListEntry<T> &other) { | |
313 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52406 times.
|
53226 | next = (other.next == &other) ? this : other.next; |
314 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 52406 times.
|
53226 | prev = (other.prev == &other) ? this : other.prev; |
315 | 53226 | } | |
316 | |||
317 | 221166 | virtual ~ListEntry() { } | |
318 | |||
319 | /** | ||
320 | * Checks if the ListEntry is the list head | ||
321 | * @return true if ListEntry is list head otherwise false | ||
322 | */ | ||
323 | virtual bool IsListHead() const = 0; | ||
324 | |||
325 | /** | ||
326 | * A lonely ListEntry has no connection to other elements. | ||
327 | * @return true if ListEntry is lonely otherwise false | ||
328 | */ | ||
329 |
3/4✓ Branch 0 taken 2197060 times.
✓ Branch 1 taken 6513370 times.
✓ Branch 2 taken 2197060 times.
✗ Branch 3 not taken.
|
8714386 | bool IsLonely() const { return (this->next == this && this->prev == this); } |
330 | |||
331 | ListEntry<T> *next; /**< Pointer to next element in the list. */ | ||
332 | ListEntry<T> *prev; /**< Pointer to previous element in the list. */ | ||
333 | |||
334 | protected: | ||
335 | /** | ||
336 | * Insert a given ListEntry after this one. | ||
337 | * @param entry the ListEntry to insert after this one | ||
338 | */ | ||
339 | inline void InsertAsSuccessor(ListEntryContent<T> *entry) { | ||
340 | assert(entry->IsLonely()); | ||
341 | |||
342 | // Mount the new element between this and this->next | ||
343 | entry->next = this->next; | ||
344 | entry->prev = this; | ||
345 | |||
346 | // Fix pointers of existing list elements | ||
347 | this->next->prev = entry; | ||
348 | this->next = entry; | ||
349 | assert(!entry->IsLonely()); | ||
350 | } | ||
351 | |||
352 | /** | ||
353 | * Insert a given ListEntry in front of this one | ||
354 | * @param entry the ListEntry to insert in front of this one | ||
355 | */ | ||
356 | 2198459 | inline void InsertAsPredecessor(ListEntryContent<T> *entry) { | |
357 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 2197060 times.
|
2198459 | assert(entry->IsLonely()); |
358 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 2197060 times.
|
2198459 | assert(!entry->IsListHead()); |
359 | |||
360 | // Mount the new element between this and this->prev | ||
361 | 2198459 | entry->next = this; | |
362 | 2198459 | entry->prev = this->prev; | |
363 | |||
364 | // Fix pointers of existing list elements | ||
365 | 2198459 | this->prev->next = entry; | |
366 | 2198459 | this->prev = entry; | |
367 | |||
368 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 2197060 times.
|
2198459 | assert(!entry->IsLonely()); |
369 | 2198459 | } | |
370 | |||
371 | /** | ||
372 | * Remove this element from it's list. | ||
373 | * The function connects this->next with this->prev leaving the list | ||
374 | * in a consistent state. The ListEntry itself is lonely afterwards, | ||
375 | * but not deleted. | ||
376 | */ | ||
377 | virtual void RemoveFromList() = 0; | ||
378 | |||
379 | private: | ||
380 | // No assignment operator (enforced by linker error) | ||
381 | ListEntry<T> &operator=(const ListEntry<T> &other); | ||
382 | }; // template<class T> class ListEntry | ||
383 | |||
384 | /** | ||
385 | * Specialized ListEntry to contain a data entry of type T | ||
386 | */ | ||
387 | template<class T> | ||
388 | class ListEntryContent : public ListEntry<T> { | ||
389 | public: | ||
390 |
1/2✓ Branch 2 taken 6811 times.
✗ Branch 3 not taken.
|
53226 | explicit ListEntryContent(T content) { content_ = content; } |
391 | |||
392 | 2261161 | inline bool IsListHead() const { return false; } | |
393 | 20281 | inline T content() const { return content_; } | |
394 | |||
395 | /** | ||
396 | * See ListEntry base class. | ||
397 | */ | ||
398 | 2161026 | inline void RemoveFromList() { | |
399 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 2160447 times.
|
2161026 | assert(!this->IsLonely()); |
400 | |||
401 | // Remove this from list | ||
402 | 2161026 | this->prev->next = this->next; | |
403 | 2161026 | this->next->prev = this->prev; | |
404 | |||
405 | // Make this lonely | ||
406 | 2161026 | this->next = this; | |
407 | 2161026 | this->prev = this; | |
408 | 2161026 | } | |
409 | |||
410 | private: | ||
411 | T content_; /**< The data content of this ListEntry */ | ||
412 | }; | ||
413 | |||
414 | /** | ||
415 | * Specialized ListEntry to form a list head. | ||
416 | * Every list has exactly one list head which is also the entry point | ||
417 | * in the list. It is used to manipulate the list. | ||
418 | */ | ||
419 | template<class T> | ||
420 | class ListEntryHead : public ListEntry<T> { | ||
421 | public: | ||
422 | 8410 | explicit ListEntryHead(ConcreteMemoryAllocator *allocator) | |
423 | 8410 | : allocator_(allocator) { } | |
424 | |||
425 | 11550 | virtual ~ListEntryHead() { this->clear(); } | |
426 | |||
427 | /** | ||
428 | * Remove all entries from the list. | ||
429 | * ListEntry objects are deleted but contained data keeps available | ||
430 | */ | ||
431 | 8430 | void clear() { | |
432 | // Delete all list entries | ||
433 | 8430 | ListEntry<T> *entry = this->next; | |
434 | ListEntry<T> *delete_me; | ||
435 |
2/2✓ Branch 1 taken 36609 times.
✓ Branch 2 taken 5797 times.
|
45859 | while (!entry->IsListHead()) { |
436 | 37429 | delete_me = entry; | |
437 | 37429 | entry = entry->next; | |
438 | 37429 | allocator_->Destruct( | |
439 | static_cast<ConcreteListEntryContent *>(delete_me)); | ||
440 | } | ||
441 | |||
442 | // Reset the list to lonely | ||
443 | 8430 | this->next = this; | |
444 | 8430 | this->prev = this; | |
445 | 8430 | } | |
446 | |||
447 | 8663 | inline bool IsListHead() const { return true; } | |
448 | 11209 | inline bool IsEmpty() const { return this->IsLonely(); } | |
449 | |||
450 | /** | ||
451 | * Push a new data object to the end of the list. | ||
452 | * @param the data object to insert | ||
453 | * @return the ListEntryContent structure wrapped around the data object | ||
454 | */ | ||
455 | 53226 | inline ListEntryContent<T> *PushBack(T content) { | |
456 |
1/2✓ Branch 1 taken 52406 times.
✗ Branch 2 not taken.
|
53226 | ListEntryContent<T> *new_entry = allocator_->Construct( |
457 | 106452 | ListEntryContent<T>(content)); | |
458 | 53226 | this->InsertAsPredecessor(new_entry); | |
459 | 53226 | return new_entry; | |
460 | } | ||
461 | |||
462 | /** | ||
463 | * Pop the first object of the list. | ||
464 | * The object is returned and removed from the list | ||
465 | * @return the data object which resided in the first list entry | ||
466 | */ | ||
467 | 11209 | inline T PopFront() { | |
468 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 11209 times.
|
11209 | assert(!this->IsEmpty()); |
469 | 11209 | return Pop(this->next); | |
470 | } | ||
471 | |||
472 | /** | ||
473 | * Take a list entry out of it's list and reinsert at the end of this list. | ||
474 | * @param the ListEntry to be moved to the end of this list | ||
475 | */ | ||
476 | 2145233 | inline void MoveToBack(ListEntryContent<T> *entry) { | |
477 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 2144654 times.
|
2145233 | assert(!entry->IsLonely()); |
478 | |||
479 | 2145233 | entry->RemoveFromList(); | |
480 | 2145233 | this->InsertAsPredecessor(entry); | |
481 | 2145233 | } | |
482 | |||
483 | /** | ||
484 | * See ListEntry base class | ||
485 | */ | ||
486 | ✗ | inline void RemoveFromList() { assert(false); } | |
487 | |||
488 | private: | ||
489 | /** | ||
490 | * Pop a ListEntry from the list (arbitrary position). | ||
491 | * The given ListEntry is removed from the list, deleted and it's | ||
492 | * data content is returned | ||
493 | * @param popped_entry the entry to be popped | ||
494 | * @return the data object of the popped ListEntry | ||
495 | */ | ||
496 | 11209 | inline T Pop(ListEntry<T> *popped_entry) { | |
497 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 11209 times.
|
11209 | assert(!popped_entry->IsListHead()); |
498 | |||
499 | 11209 | ListEntryContent<T> *popped = (ListEntryContent<T> *)popped_entry; | |
500 | 11209 | popped->RemoveFromList(); | |
501 | 11209 | T result = popped->content(); | |
502 | 11209 | allocator_->Destruct( | |
503 | static_cast<ConcreteListEntryContent *>(popped_entry)); | ||
504 | 11209 | return result; | |
505 | } | ||
506 | |||
507 | private: | ||
508 | ConcreteMemoryAllocator *allocator_; | ||
509 | }; | ||
510 | |||
511 | public: // LruCache | ||
512 | /** | ||
513 | * Create a new LRU cache object | ||
514 | * @param cache_size the maximal size of the cache | ||
515 | */ | ||
516 | 8410 | LruCache(const unsigned cache_size, | |
517 | const Key &empty_key, | ||
518 | uint32_t (*hasher)(const Key &key), | ||
519 | perf::StatisticsTemplate statistics) | ||
520 |
1/2✓ Branch 1 taken 5777 times.
✗ Branch 2 not taken.
|
8410 | : counters_(statistics) |
521 | 8410 | , pause_(false) | |
522 | 8410 | , cache_gauge_(0) | |
523 | 8410 | , cache_size_(cache_size) | |
524 | 8410 | , allocator_(cache_size) | |
525 |
2/4✓ Branch 3 taken 5777 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 5777 times.
✗ Branch 7 not taken.
|
16820 | , lru_list_(&allocator_) { |
526 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5777 times.
|
8410 | assert(cache_size > 0); |
527 | |||
528 | 8410 | counters_.sz_size->Set(cache_size_); | |
529 | 8410 | filter_entry_ = NULL; | |
530 | // cache_ = Cache(cache_size_); | ||
531 |
1/2✓ Branch 1 taken 5777 times.
✗ Branch 2 not taken.
|
8410 | cache_.Init(cache_size_, empty_key, hasher); |
532 | 8410 | perf::Xadd(counters_.sz_allocated, | |
533 | 8410 | allocator_.bytes_allocated() + cache_.bytes_allocated()); | |
534 | |||
535 | #ifdef LRU_CACHE_THREAD_SAFE | ||
536 | 8410 | const int retval = pthread_mutex_init(&lock_, NULL); | |
537 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5777 times.
|
8410 | assert(retval == 0); |
538 | #endif | ||
539 | 8410 | } | |
540 | |||
541 | 4686 | static double GetEntrySize() { | |
542 | 4686 | return SmallHashFixed<Key, CacheEntry>::GetEntrySize() | |
543 | 4686 | + ConcreteMemoryAllocator::GetEntrySize(); | |
544 | } | ||
545 | |||
546 | 11550 | virtual ~LruCache() { | |
547 | #ifdef LRU_CACHE_THREAD_SAFE | ||
548 | 11550 | pthread_mutex_destroy(&lock_); | |
549 | #endif | ||
550 | } | ||
551 | |||
552 | /** | ||
553 | * Insert a new key-value pair to the list. | ||
554 | * If the cache is already full, the least recently used object is removed; | ||
555 | * afterwards the new object is inserted. | ||
556 | * If the object is already present it is updated and moved back to the end | ||
557 | * of the list | ||
558 | * @param key the key where the value is saved | ||
559 | * @param value the value of the cache entry | ||
560 | * @return true on insert, false on update | ||
561 | */ | ||
562 | 1113489 | virtual bool Insert(const Key &key, const Value &value) { | |
563 | 1113489 | this->Lock(); | |
564 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 1112647 times.
|
1113489 | if (pause_) { |
565 | 22 | Unlock(); | |
566 | 22 | return false; | |
567 | } | ||
568 | |||
569 |
1/2✓ Branch 1 taken 1066953 times.
✗ Branch 2 not taken.
|
1113467 | CacheEntry entry; |
570 | |||
571 | // Check if we have to update an existent entry | ||
572 |
3/4✓ Branch 1 taken 1112647 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1060241 times.
✓ Branch 4 taken 52406 times.
|
1113467 | if (this->DoLookup(key, &entry)) { |
573 | 1060241 | perf::Inc(counters_.n_update); | |
574 |
1/2✓ Branch 1 taken 99 times.
✗ Branch 2 not taken.
|
1060241 | entry.value = value; |
575 |
1/2✓ Branch 1 taken 1060241 times.
✗ Branch 2 not taken.
|
1060241 | cache_.Insert(key, entry); |
576 |
1/2✓ Branch 1 taken 1060241 times.
✗ Branch 2 not taken.
|
1060241 | this->Touch(entry); |
577 | 1060241 | this->Unlock(); | |
578 | 1060241 | return false; | |
579 | } | ||
580 | |||
581 | 53226 | perf::Inc(counters_.n_insert); | |
582 | // Check if we have to make some space in the cache a | ||
583 |
2/2✓ Branch 1 taken 11209 times.
✓ Branch 2 taken 41197 times.
|
53226 | if (this->IsFull()) |
584 |
1/2✓ Branch 1 taken 11209 times.
✗ Branch 2 not taken.
|
11209 | this->DeleteOldest(); |
585 | |||
586 |
1/2✓ Branch 1 taken 52406 times.
✗ Branch 2 not taken.
|
53226 | entry.list_entry = lru_list_.PushBack(key); |
587 |
1/2✓ Branch 1 taken 46415 times.
✗ Branch 2 not taken.
|
53226 | entry.value = value; |
588 | |||
589 |
1/2✓ Branch 1 taken 52406 times.
✗ Branch 2 not taken.
|
53226 | cache_.Insert(key, entry); |
590 | 53226 | cache_gauge_++; | |
591 | |||
592 | 53226 | Unlock(); | |
593 | 53226 | return true; | |
594 | 47334 | } | |
595 | |||
596 | |||
597 | /** | ||
598 | * Updates object and moves back to the end of the list. The object must be | ||
599 | * present. | ||
600 | */ | ||
601 | 11 | virtual void Update(const Key &key) { | |
602 | 11 | Lock(); | |
603 | // Is not called from the client, only from the cache plugin | ||
604 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
|
11 | assert(!pause_); |
605 |
0/2✗ Branch 1 not taken.
✗ Branch 2 not taken.
|
11 | CacheEntry entry; |
606 |
1/2✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
|
11 | const bool retval = DoLookup(key, &entry); |
607 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
|
11 | assert(retval); |
608 | 11 | perf::Inc(counters_.n_update); | |
609 |
1/2✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
|
11 | Touch(entry); |
610 | 11 | Unlock(); | |
611 | 11 | } | |
612 | |||
613 | |||
614 | /** | ||
615 | * Changes the value of an entry in the LRU cache without updating the LRU | ||
616 | * order. | ||
617 | */ | ||
618 | 33 | virtual bool UpdateValue(const Key &key, const Value &value) { | |
619 | 33 | this->Lock(); | |
620 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
|
33 | if (pause_) { |
621 | ✗ | Unlock(); | |
622 | ✗ | return false; | |
623 | } | ||
624 | |||
625 |
0/2✗ Branch 1 not taken.
✗ Branch 2 not taken.
|
33 | CacheEntry entry; |
626 |
3/4✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
✓ Branch 4 taken 22 times.
|
33 | if (!this->DoLookup(key, &entry)) { |
627 | 11 | this->Unlock(); | |
628 | 11 | return false; | |
629 | } | ||
630 | |||
631 | 22 | perf::Inc(counters_.n_update_value); | |
632 |
1/2✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
|
22 | entry.value = value; |
633 |
1/2✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
|
22 | cache_.Insert(key, entry); |
634 | 22 | this->Unlock(); | |
635 | 22 | return true; | |
636 | 33 | } | |
637 | |||
638 | |||
639 | /** | ||
640 | * Retrieve an element from the cache. | ||
641 | * If the element was found, it will be marked as 'recently used' and returned | ||
642 | * @param key the key to perform a lookup on | ||
643 | * @param value (out) here the result is saved (not touch in case of miss) | ||
644 | * @return true on successful lookup, false if key was not found | ||
645 | */ | ||
646 | 1096391 | virtual bool Lookup(const Key &key, Value *value, bool update_lru = true) { | |
647 | 1096391 | bool found = false; | |
648 | 1096391 | Lock(); | |
649 |
2/2✓ Branch 0 taken 220 times.
✓ Branch 1 taken 1094772 times.
|
1096391 | if (pause_) { |
650 | 220 | Unlock(); | |
651 | 220 | return false; | |
652 | } | ||
653 | |||
654 |
1/2✓ Branch 1 taken 1071089 times.
✗ Branch 2 not taken.
|
1096171 | CacheEntry entry; |
655 |
3/4✓ Branch 1 taken 1094772 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1085821 times.
✓ Branch 4 taken 8951 times.
|
1096171 | if (DoLookup(key, &entry)) { |
656 | // Hit | ||
657 | 1086400 | perf::Inc(counters_.n_hit); | |
658 |
2/2✓ Branch 0 taken 1084402 times.
✓ Branch 1 taken 1419 times.
|
1086400 | if (update_lru) |
659 |
1/2✓ Branch 1 taken 1084402 times.
✗ Branch 2 not taken.
|
1084981 | Touch(entry); |
660 |
1/2✓ Branch 1 taken 23789 times.
✗ Branch 2 not taken.
|
1086400 | *value = entry.value; |
661 | 1086400 | found = true; | |
662 | } else { | ||
663 | 9771 | perf::Inc(counters_.n_miss); | |
664 | } | ||
665 | |||
666 | 1096171 | Unlock(); | |
667 | 1096171 | return found; | |
668 | 26481 | } | |
669 | |||
670 | /** | ||
671 | * Forgets about a specific cache entry | ||
672 | * @param key the key to delete from the cache | ||
673 | * @return true if key was deleted, false if key was not in the cache | ||
674 | */ | ||
675 | 384 | virtual bool Forget(const Key &key) { | |
676 | 384 | bool found = false; | |
677 | 384 | this->Lock(); | |
678 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 362 times.
|
384 | if (pause_) { |
679 | 22 | Unlock(); | |
680 | 22 | return false; | |
681 | } | ||
682 | |||
683 |
1/2✓ Branch 1 taken 164 times.
✗ Branch 2 not taken.
|
362 | CacheEntry entry; |
684 |
3/4✓ Branch 1 taken 362 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 285 times.
✓ Branch 4 taken 77 times.
|
362 | if (this->DoLookup(key, &entry)) { |
685 | 285 | found = true; | |
686 | 285 | perf::Inc(counters_.n_forget); | |
687 | |||
688 |
1/2✓ Branch 1 taken 285 times.
✗ Branch 2 not taken.
|
285 | entry.list_entry->RemoveFromList(); |
689 |
1/2✓ Branch 1 taken 285 times.
✗ Branch 2 not taken.
|
285 | allocator_.Destruct(entry.list_entry); |
690 |
1/2✓ Branch 1 taken 285 times.
✗ Branch 2 not taken.
|
285 | cache_.Erase(key); |
691 | 285 | --cache_gauge_; | |
692 | } | ||
693 | |||
694 | 362 | this->Unlock(); | |
695 | 362 | return found; | |
696 | 198 | } | |
697 | |||
698 | /** | ||
699 | * Clears all elements from the cache. | ||
700 | * All memory of internal data structures will be freed but data of | ||
701 | * cache entries may stay in use, we do not call delete on any user data. | ||
702 | */ | ||
703 | 22 | virtual void Drop() { | |
704 | 22 | this->Lock(); | |
705 | |||
706 | 22 | cache_gauge_ = 0; | |
707 | 22 | lru_list_.clear(); | |
708 | 22 | cache_.Clear(); | |
709 | 22 | perf::Inc(counters_.n_drop); | |
710 | 22 | counters_.sz_allocated->Set(0); | |
711 | 22 | perf::Xadd(counters_.sz_allocated, | |
712 | 22 | allocator_.bytes_allocated() + cache_.bytes_allocated()); | |
713 | |||
714 | 22 | this->Unlock(); | |
715 | 22 | } | |
716 | |||
717 | 65 | void Pause() { | |
718 | 65 | Lock(); | |
719 | 65 | pause_ = true; | |
720 | 65 | Unlock(); | |
721 | 65 | } | |
722 | |||
723 | 33 | void Resume() { | |
724 | 33 | Lock(); | |
725 | 33 | pause_ = false; | |
726 | 33 | Unlock(); | |
727 | 33 | } | |
728 | |||
729 | 109920 | inline bool IsFull() const { return cache_gauge_ >= cache_size_; } | |
730 | 11649 | inline bool IsEmpty() const { return cache_gauge_ == 0; } | |
731 | |||
732 | Counters counters() { | ||
733 | Lock(); | ||
734 | cache_.GetCollisionStats(&counters_.num_collisions, | ||
735 | &counters_.max_collisions); | ||
736 | Unlock(); | ||
737 | return counters_; | ||
738 | } | ||
739 | |||
740 | /** | ||
741 | * Prepares for in-order iteration of the cache entries to perform a filter | ||
742 | * operation. To ensure consistency, the LruCache must be locked for the | ||
743 | * duration of the filter operation. | ||
744 | */ | ||
745 | 474 | virtual void FilterBegin() { | |
746 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 474 times.
|
474 | assert(!filter_entry_); |
747 | 474 | Lock(); | |
748 | 474 | filter_entry_ = &lru_list_; | |
749 | 474 | } | |
750 | |||
751 | /** | ||
752 | * Get the current key and value for the filter operation | ||
753 | * @param key Address to write the key | ||
754 | * @param value Address to write the value | ||
755 | */ | ||
756 | 4773 | virtual void FilterGet(Key *key, Value *value) { | |
757 |
1/2✓ Branch 1 taken 4751 times.
✗ Branch 2 not taken.
|
4773 | CacheEntry entry; |
758 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4773 times.
|
4773 | assert(filter_entry_); |
759 |
2/4✓ Branch 1 taken 4773 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 4773 times.
|
4773 | assert(!filter_entry_->IsListHead()); |
760 | 4773 | *key = static_cast<ConcreteListEntryContent *>(filter_entry_)->content(); | |
761 |
1/2✓ Branch 1 taken 4773 times.
✗ Branch 2 not taken.
|
4773 | const bool rc = this->DoLookup(*key, &entry); |
762 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4773 times.
|
4773 | assert(rc); |
763 |
1/2✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
|
4773 | *value = entry.value; |
764 | 4773 | } | |
765 | |||
766 | /** | ||
767 | * Advance to the next entry in the list | ||
768 | * @returns false upon reaching the end of the cache list | ||
769 | */ | ||
770 | 5225 | virtual bool FilterNext() { | |
771 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 5225 times.
|
5225 | assert(filter_entry_); |
772 | 5225 | filter_entry_ = filter_entry_->next; | |
773 | 5225 | return !filter_entry_->IsListHead(); | |
774 | } | ||
775 | |||
776 | /** | ||
777 | * Delete the current cache list entry | ||
778 | */ | ||
779 | 4299 | virtual void FilterDelete() { | |
780 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4299 times.
|
4299 | assert(filter_entry_); |
781 |
2/4✓ Branch 1 taken 4299 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 4299 times.
|
4299 | assert(!filter_entry_->IsListHead()); |
782 | 4299 | ListEntry<Key> *new_current = filter_entry_->prev; | |
783 | 4299 | perf::Inc(counters_.n_forget); | |
784 | 4299 | Key k = static_cast<ConcreteListEntryContent *>(filter_entry_)->content(); | |
785 |
1/2✓ Branch 1 taken 4299 times.
✗ Branch 2 not taken.
|
4299 | filter_entry_->RemoveFromList(); |
786 |
1/2✓ Branch 1 taken 4299 times.
✗ Branch 2 not taken.
|
4299 | allocator_.Destruct(static_cast<ConcreteListEntryContent *>(filter_entry_)); |
787 |
1/2✓ Branch 1 taken 4299 times.
✗ Branch 2 not taken.
|
4299 | cache_.Erase(k); |
788 | 4299 | --cache_gauge_; | |
789 | 4299 | filter_entry_ = new_current; | |
790 | 4299 | } | |
791 | |||
792 | /** | ||
793 | * Finish filtering the entries and unlock the cache | ||
794 | */ | ||
795 | 474 | virtual void FilterEnd() { | |
796 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 474 times.
|
474 | assert(filter_entry_); |
797 | 474 | filter_entry_ = NULL; | |
798 | 474 | Unlock(); | |
799 | 474 | } | |
800 | |||
801 | protected: | ||
802 | Counters counters_; | ||
803 | |||
804 | private: | ||
805 | /** | ||
806 | * this just performs a lookup in the cache | ||
807 | * WITHOUT changing the LRU order | ||
808 | * @param key the key to perform a lookup on | ||
809 | * @param entry a pointer to the entry structure | ||
810 | * @return true on successful lookup, false otherwise | ||
811 | */ | ||
812 | 2214817 | inline bool DoLookup(const Key &key, CacheEntry *entry) { | |
813 | 2214817 | return cache_.Lookup(key, entry); | |
814 | } | ||
815 | |||
816 | /** | ||
817 | * Touch an entry. | ||
818 | * The entry will be moved to the back of the LRU list to mark it | ||
819 | * as 'recently used'... this saves the entry from being deleted | ||
820 | * @param entry the CacheEntry to be touched (CacheEntry is the internal | ||
821 | * wrapper data structure) | ||
822 | */ | ||
823 | 2145233 | inline void Touch(const CacheEntry &entry) { | |
824 | 2145233 | lru_list_.MoveToBack(entry.list_entry); | |
825 | 2145233 | } | |
826 | |||
827 | /** | ||
828 | * Deletes the least recently used entry from the cache. | ||
829 | */ | ||
830 | 11209 | inline void DeleteOldest() { | |
831 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 11209 times.
|
11209 | assert(!this->IsEmpty()); |
832 | |||
833 | 11209 | perf::Inc(counters_.n_replace); | |
834 |
1/2✓ Branch 1 taken 11209 times.
✗ Branch 2 not taken.
|
11209 | Key delete_me = lru_list_.PopFront(); |
835 |
1/2✓ Branch 1 taken 11209 times.
✗ Branch 2 not taken.
|
11209 | cache_.Erase(delete_me); |
836 | |||
837 | 11209 | --cache_gauge_; | |
838 | 11209 | } | |
839 | |||
840 | /** | ||
841 | * Locks the cache (thread safety). | ||
842 | */ | ||
843 | 2210902 | inline void Lock() { | |
844 | #ifdef LRU_CACHE_THREAD_SAFE | ||
845 | 2210902 | pthread_mutex_lock(&lock_); | |
846 | #endif | ||
847 | 2210902 | } | |
848 | |||
849 | /** | ||
850 | * Unlocks the cache (thread safety). | ||
851 | */ | ||
852 | 2210902 | inline void Unlock() { | |
853 | #ifdef LRU_CACHE_THREAD_SAFE | ||
854 | 2210902 | pthread_mutex_unlock(&lock_); | |
855 | #endif | ||
856 | 2210902 | } | |
857 | |||
858 | bool pause_; /**< Temporarily stops the cache in order to avoid poisoning */ | ||
859 | |||
860 | // Internal data fields | ||
861 | unsigned int cache_gauge_; | ||
862 | const unsigned int cache_size_; | ||
863 | ConcreteMemoryAllocator allocator_; | ||
864 | |||
865 | /** | ||
866 | * A doubly linked list to keep track of the least recently used data entries. | ||
867 | * New entries get pushed back to the list. If an entry is touched | ||
868 | * it is moved to the back of the list again. | ||
869 | * If the cache gets too long, the first element (the oldest) gets | ||
870 | * deleted to obtain some space. | ||
871 | */ | ||
872 | ListEntryHead<Key> lru_list_; | ||
873 | SmallHashFixed<Key, CacheEntry> cache_; | ||
874 | |||
875 | ListEntry<Key> *filter_entry_; | ||
876 | #ifdef LRU_CACHE_THREAD_SAFE | ||
877 | pthread_mutex_t lock_; /**< Mutex to make cache thread safe. */ | ||
878 | #endif | ||
879 | }; // class LruCache | ||
880 | |||
881 | } // namespace lru | ||
882 | |||
883 | #endif // CVMFS_LRU_H_ | ||
884 |