Skip to content

Commit

Permalink
feat(autoware_universe_utils): add LRU Cache (#8456)
Browse files Browse the repository at this point in the history
Signed-off-by: Y.Hisaki <yhisaki31@gmail.com>
  • Loading branch information
yhisaki authored Aug 13, 2024
1 parent bbc8525 commit ca1d101
Show file tree
Hide file tree
Showing 3 changed files with 293 additions and 0 deletions.
73 changes: 73 additions & 0 deletions common/autoware_universe_utils/examples/example_lru_cache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "autoware/universe_utils/system/lru_cache.hpp"

#include <chrono>
#include <cstdint>
#include <iostream>

using autoware::universe_utils::LRUCache;

// Fibonacci calculation with LRU cache
int64_t fibonacci_with_cache(int n, LRUCache<int, int64_t> * cache)
{
if (n <= 1) return n;

if (cache->contains(n)) {
return *cache->get(n);
}
int64_t result = fibonacci_with_cache(n - 1, cache) + fibonacci_with_cache(n - 2, cache);
cache->put(n, result);
return result;
}

// Fibonacci calculation without cache
int64_t fibonacci_no_cache(int n)
{
if (n <= 1) return n;
return fibonacci_no_cache(n - 1) + fibonacci_no_cache(n - 2);
}

// Helper function to measure execution time
template <typename Func, typename... Args>
int64_t measure_time(Func func, Args &&... args)
{
auto start = std::chrono::high_resolution_clock::now();
func(std::forward<Args>(args)...);
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}

int main()
{
const int max_n = 40; // Increased for more noticeable time difference
LRUCache<int, int64_t> cache(max_n); // Cache size set to MAX_N

std::cout << "Comparing Fibonacci calculation time with and without LRU cache:\n\n";
std::cout << "n\tWith Cache (μs)\tWithout Cache (μs)\n";
std::cout << std::string(43, '-') << "\n";

for (int i = 30; i <= max_n; ++i) { // Starting from 30 for more significant differences
int64_t time_with_cache = measure_time([i, &cache]() { fibonacci_with_cache(i, &cache); });
int64_t time_without_cache = measure_time([i]() { fibonacci_no_cache(i); });

std::cout << i << "\t" << time_with_cache << "\t\t" << time_without_cache << "\n";

// Clear the cache after each iteration to ensure fair comparison
cache.clear();
}

return 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef AUTOWARE__UNIVERSE_UTILS__SYSTEM__LRU_CACHE_HPP_
#define AUTOWARE__UNIVERSE_UTILS__SYSTEM__LRU_CACHE_HPP_

#include <cstddef>
#include <list>
#include <optional>
#include <unordered_map>
#include <utility>

namespace autoware::universe_utils
{

/**
* @brief A template class for LRU (Least Recently Used) Cache.
*
* This class implements a simple LRU cache using a combination of a list and a hash map.
*
* @tparam Key The type of keys.
* @tparam Value The type of values.
* @tparam Map The type of underlying map, defaulted to std::unordered_map.
*/
template <typename Key, typename Value, template <typename...> class Map = std::unordered_map>
class LRUCache
{
private:
size_t capacity_; ///< The maximum capacity of the cache.
std::list<std::pair<Key, Value>> cache_list_; ///< List to maintain the order of elements.
Map<Key, typename std::list<std::pair<Key, Value>>::iterator>
cache_map_; ///< Map for fast access to elements.

public:
/**
* @brief Construct a new LRUCache object.
*
* @param size The capacity of the cache.
*/
explicit LRUCache(size_t size) : capacity_(size) {}

/**
* @brief Get the capacity of the cache.
*
* @return The capacity of the cache.
*/
[[nodiscard]] size_t capacity() const { return capacity_; }

/**
* @brief Insert a key-value pair into the cache.
*
* If the key already exists, its value is updated and it is moved to the front.
* If the cache exceeds its capacity, the least recently used element is removed.
*
* @param key The key to insert.
* @param value The value to insert.
*/
void put(const Key & key, const Value & value)
{
auto it = cache_map_.find(key);
if (it != cache_map_.end()) {
cache_list_.erase(it->second);
}
cache_list_.push_front({key, value});
cache_map_[key] = cache_list_.begin();

if (cache_map_.size() > capacity_) {
auto last = cache_list_.back();
cache_map_.erase(last.first);
cache_list_.pop_back();
}
}

/**
* @brief Retrieve a value from the cache.
*
* If the key does not exist in the cache, std::nullopt is returned.
* If the key exists, the value is returned and the element is moved to the front.
*
* @param key The key to retrieve.
* @return The value associated with the key, or std::nullopt if the key does not exist.
*/
std::optional<Value> get(const Key & key)
{
auto it = cache_map_.find(key);
if (it == cache_map_.end()) {
return std::nullopt;
}
cache_list_.splice(cache_list_.begin(), cache_list_, it->second);
return it->second->second;
}

/**
* @brief Clear the cache.
*
* This removes all elements from the cache.
*/
void clear()
{
cache_list_.clear();
cache_map_.clear();
}

/**
* @brief Get the current size of the cache.
*
* @return The number of elements in the cache.
*/
[[nodiscard]] size_t size() const { return cache_map_.size(); }

/**
* @brief Check if the cache is empty.
*
* @return True if the cache is empty, false otherwise.
*/
[[nodiscard]] bool empty() const { return cache_map_.empty(); }

/**
* @brief Check if a key exists in the cache.
*
* @param key The key to check.
* @return True if the key exists, false otherwise.
*/
[[nodiscard]] bool contains(const Key & key) const
{
return cache_map_.find(key) != cache_map_.end();
}
};

} // namespace autoware::universe_utils

#endif // AUTOWARE__UNIVERSE_UTILS__SYSTEM__LRU_CACHE_HPP_
78 changes: 78 additions & 0 deletions common/autoware_universe_utils/test/src/system/test_lru_cache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "autoware/universe_utils/system/lru_cache.hpp"

#include <gtest/gtest.h>

#include <chrono>
#include <cstdint>
#include <iostream>
#include <utility>

using autoware::universe_utils::LRUCache;

// Fibonacci calculation with LRU cache
int64_t fibonacci_with_cache(int n, LRUCache<int, int64_t> * cache)
{
if (n <= 1) return n;

if (cache->contains(n)) {
return *cache->get(n);
}
int64_t result = fibonacci_with_cache(n - 1, cache) + fibonacci_with_cache(n - 2, cache);
cache->put(n, result);
return result;
}

// Fibonacci calculation without cache
int64_t fibonacci_no_cache(int n)
{
if (n <= 1) return n;
return fibonacci_no_cache(n - 1) + fibonacci_no_cache(n - 2);
}

// Helper function to measure execution time
template <typename Func, typename... Args>
std::pair<int64_t, decltype(std::declval<Func>()(std::declval<Args>()...))> measure_time(
Func func, Args &&... args)
{
auto start = std::chrono::high_resolution_clock::now();
auto result = func(std::forward<Args>(args)...);
auto end = std::chrono::high_resolution_clock::now();
return {std::chrono::duration_cast<std::chrono::microseconds>(end - start).count(), result};
}

// Test case to verify Fibonacci calculation results with and without cache
TEST(FibonacciLRUCacheTest, CompareResultsAndPerformance)
{
const int max_n = 40; // Test range
LRUCache<int, int64_t> cache(max_n); // Cache with capacity set to max_n

for (int i = 0; i <= max_n; ++i) {
// Measure time for performance comparison
auto [time_with_cache, result_with_cache] =
measure_time([i, &cache]() { return fibonacci_with_cache(i, &cache); });
auto [time_without_cache, result_without_cache] =
measure_time([i]() { return fibonacci_no_cache(i); });

EXPECT_EQ(result_with_cache, result_without_cache) << "Mismatch at n = " << i;

// Print the calculation time
std::cout << "n = " << i << ": With Cache = " << time_with_cache
<< " μs, Without Cache = " << time_without_cache << " μs\n";

// // Clear the cache after each iteration to ensure fair comparison
cache.clear();
}
}

0 comments on commit ca1d101

Please sign in to comment.