Skip to content

Commit

Permalink
Add unittest and bench sample
Browse files Browse the repository at this point in the history
  • Loading branch information
kassane committed May 20, 2024
1 parent a95477c commit 7917e17
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 43 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ jobs:
- name: CMake - Build
run: cmake --build build/ --parallel
- name: CMake - Test
run: ctest --test-dir build/ -C Debug
run: cmake --build build/ --target unittest
- name: CMake - Run benchmark (dbg mode)
run: cmake --build build/ --target bench
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@
*.app

build/
Testing/
Testing/
.cache/
63 changes: 54 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,60 @@
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.8...3.27)

if(${CMAKE_VERSION} VERSION_LESS 3.14)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

project(SieveCache VERSION 1.0)

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Choose default type of build (Debug)" FORCE)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
elseif(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
set(CMAKE_CONFIGURATION_TYPES "Release")
else()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(sieve_cache source/sieve.cc)
if(MSVC)
target_compile_options(sieve_cache PRIVATE /W4 /WX)
else()
target_compile_options(sieve_cache PRIVATE -Wall -Wextra -pedantic -Werror)
option(BUILD_BENCH "Build bench" ON)
option(BUILD_TESTS "Build tests" ON)
include(cmake/doctest.cmake)

include_directories("include")

if(BUILD_TESTS)
add_executable(${PROJECT_NAME}_test "tests/test.cc")
target_include_directories(${PROJECT_NAME}_test INTERFACE "include")
target_link_libraries(${PROJECT_NAME}_test PRIVATE doctest::doctest)

if(MSVC)
target_compile_options(${PROJECT_NAME}_test PRIVATE /W4 /WX)
else()
target_compile_options(${PROJECT_NAME}_test PRIVATE -Wall -Wextra -pedantic -Werror)
endif()

add_custom_target(unittest
COMMAND ${PROJECT_NAME}_test
DEPENDS ${PROJECT_NAME}_test
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endif()
if(BUILD_BENCH)
add_executable(${PROJECT_NAME}_bench "bench/bench.cc")
target_include_directories(${PROJECT_NAME}_bench INTERFACE "include")

if(MSVC)
target_compile_options(${PROJECT_NAME}_bench PRIVATE /W4 /WX)
else()
target_compile_options(${PROJECT_NAME}_bench PRIVATE -Wall -Wextra -pedantic -Werror)
endif()

enable_testing()
add_test(NAME SieveCacheTest COMMAND sieve_cache)
add_custom_target(bench
COMMAND ${PROJECT_NAME}_bench
DEPENDS ${PROJECT_NAME}_bench
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endif()
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME})
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# Sieve Cache in C++
# Sieve Cache in C++ (header only)

A [SIEVE cache implementation](https://cachemon.github.io/SIEVE-website/) for C++. Based on [D implementation](https://github.com/kubo39/sieve-cache-d).
A [SIEVE cache](https://cachemon.github.io/SIEVE-website/) implementation for C++. Based on [D implementation](https://github.com/kubo39/sieve-cache-d).

### How to use

```bash
$ cmake -B build
$ cmake --build build
$ ctest --test-dir build
# Run benchmark sample
$ cmake --build build --target bench
# Run unittest
$ cmake --build build --target unittest
```

## LICENSE

See: [LICENSE](LICENSE)
81 changes: 81 additions & 0 deletions bench/bench.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include <iostream>
#include <chrono>
#include <random>
#include <vector>
#include <sieve.hpp>

struct S {
S() = default;
explicit S(const std::vector<unsigned char>& vec, unsigned long value) : a(vec), b(value) {}

std::vector<unsigned char> a;
unsigned long b;
};

int main() {
auto start = std::chrono::high_resolution_clock::now();

// Sequence.
{
SieveCache<unsigned long, unsigned long> cache(68);

for (int i = 1; i < 1000; ++i) {
const auto n = i % 100;
cache[n] = n;
}

for (int i = 1; i < 1000; ++i) {
const auto n = i % 100;
cache.get(n);
}
}

auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::cout << "Sequence: " << elapsed_seconds.count() << "s\n";

// Composite.
{
SieveCache<unsigned long, S> cache(68);
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<unsigned long> dist(0, 99);

for (int i = 1; i < 1000; ++i) {
const auto n = dist(rng);
cache[n] = S(std::vector<unsigned char>(12), n);
}

for (int i = 1; i < 1000; ++i) {
const auto n = dist(rng);
cache.get(n);
}
}

end = std::chrono::high_resolution_clock::now();
elapsed_seconds = end - start;
std::cout << "Composite: " << elapsed_seconds.count() << "s\n";

// CompositeNormal.
{
constexpr double SIGMA = 50.0 / 3.0;
SieveCache<unsigned long, S> cache(static_cast<size_t>(SIGMA));
std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<unsigned long> dist(0, 99);

for (int i = 1; i < 1000; ++i) {
const auto n = dist(rng);
cache[n] = S(std::vector<unsigned char>(12), n);
}

for (int i = 1; i < 1000; ++i) {
const auto n = dist(rng);
cache.get(n);
}
}

end = std::chrono::high_resolution_clock::now();
elapsed_seconds = end - start;
std::cout << "CompositeNormal: " << elapsed_seconds.count() << "s\n";

return 0;
}
10 changes: 10 additions & 0 deletions cmake/doctest.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
include(FetchContent)
if(BUILD_TESTS)
FetchContent_Declare(
doc
GIT_REPOSITORY "https://github.com/doctest/doctest.git"
GIT_TAG v2.4.11
)
FetchContent_GetProperties(doc)
FetchContent_MakeAvailable(doc)
endif()
48 changes: 19 additions & 29 deletions source/sieve.cc → include/sieve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
* License: MIT
*/

#ifndef SIEVE_HPP
#define SIEVE_HPP
#include <unordered_map>
#include <memory>
#include <cassert>
#include <string>
#include <atomic>

template <typename K, typename V>
Expand Down Expand Up @@ -86,6 +87,22 @@ class SieveCache {
length_ = 0;
}

V& operator[](const K& key) {
auto it = cache_.find(key);
if (it != cache_.end()) {
it->second->visited = true;
return it->second->value;
}
if (length_.load() >= capacity_) {
evict();
}
auto node = std::make_shared<Node>(key, V());
addNode(node);
cache_[key] = node;
length_++;
return node->value;
}

private:
struct Node {
K key;
Expand Down Expand Up @@ -154,31 +171,4 @@ class SieveCache {
std::atomic<size_t> length_;
std::unordered_map<K, std::shared_ptr<Node>> cache_;
};

int main() {
SieveCache<std::string, std::string> cache(3);
assert(cache.capacity() == 3);
assert(cache.empty());
assert(cache.insert("foo", "foocontent"));
assert(cache.insert("bar", "barcontent"));
assert(cache.remove("bar"));
assert(cache.insert("bar2", "bar2content"));
assert(cache.insert("bar3", "bar3content"));
assert(*cache.get("foo") == "foocontent");
assert(cache.contains("foo"));
assert(cache.get("bar") == nullptr);
assert(*cache.get("bar2") == "bar2content");
assert(*cache.get("bar3") == "bar3content");
assert(cache.length() == 3);
cache.clear();
assert(cache.length() == 0);
assert(!cache.contains("foo"));

// Additional test for updating visited flag
cache.insert("key1", "value1");
cache.insert("key2", "value2");
cache.insert("key1", "updated");
cache.insert("key3", "value3");
assert(cache.contains("key1"));
return 0;
}
#endif // SIEVE_HPP
87 changes: 87 additions & 0 deletions tests/test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
#include <sieve.hpp>

TEST_CASE("Testing SieveCache functionality") {
SieveCache<std::string, std::string> cache(3);

SUBCASE("Initial state checks") {
CHECK(cache.capacity() == 3);
CHECK(cache.empty());
}

SUBCASE("Insert and remove operations") {
CHECK(cache.insert("foo", "foocontent"));
CHECK(cache.insert("bar", "barcontent"));
CHECK(cache.remove("bar"));
CHECK(cache.insert("bar2", "bar2content"));
CHECK(cache.insert("bar3", "bar3content"));
}

SUBCASE("Content verification after operations") {
cache.insert("foo", "foocontent");
cache.insert("bar", "barcontent");
cache.remove("bar");
cache.insert("bar2", "bar2content");
cache.insert("bar3", "bar3content");
CHECK(*cache.get("foo") == "foocontent");
CHECK(cache.contains("foo"));
CHECK(cache.get("bar") == nullptr);
CHECK(*cache.get("bar2") == "bar2content");
CHECK(*cache.get("bar3") == "bar3content");
CHECK(cache.length() == 3);
}

SUBCASE("Clear cache and check state") {
cache.insert("foo", "foocontent");
cache.insert("bar", "barcontent");
cache.remove("bar");
cache.insert("bar2", "bar2content");
cache.insert("bar3", "bar3content");
cache.clear();
CHECK(cache.length() == 0);
CHECK(!cache.contains("foo"));
}

SUBCASE("Updating visited flag") {
cache.insert("key1", "value1");
cache.insert("key2", "value2");
cache.insert("key1", "updated");
cache.insert("key3", "value3");
CHECK(cache.contains("key1"));
}
SUBCASE("Operator[] test") {
cache["key1"] = "value1";
CHECK(cache.contains("key1"));
CHECK(*cache.get("key1") == "value1");

cache["key1"] = "updated";
CHECK(*cache.get("key1") == "updated");

cache["key2"] = "value2";
cache["key3"] = "value3";
CHECK(cache.length() == 3);

// Insert a new key and check for evictions
cache["key4"] = "value4";
CHECK(cache.length() == 3);

// Verify that one of the initial keys was evicted
bool key1_evicted = !cache.contains("key1");
bool key2_evicted = !cache.contains("key2");
bool key3_evicted = !cache.contains("key3");

// Ensure that exactly one of the keys is evicted (only key2 is evicted)
int evicted_count = key1_evicted + key2_evicted + key3_evicted;
CHECK(evicted_count == 1);

// Individually check each condition to understand which key was evicted
if (!key1_evicted) {
CHECK(*cache.get("key1") == "updated");
}
if (!key3_evicted) {
CHECK(*cache.get("key3") == "value3");
}
CHECK(*cache.get("key4") == "value4");
}
}

0 comments on commit 7917e17

Please sign in to comment.