Skip to content

Commit

Permalink
Hashy CubeSwapper
Browse files Browse the repository at this point in the history
Implement way to temporally dump the cube data into disk storage
in order to save system memory.

For `./cubes -n 13 -w -s -u` run

heaptrack tool reports:
- total runtime: 26min 18s
- peak RSS: 2.4 Gb
- peak heap memory: 978 Mb

This confirms that only the std::unordered_set<> internal
nodes (and the lookup array) are kept in memory.
Slow down is expected as accessing an element reads it from the disk.

The swap files are named as `storage_<number>.bin` in the cache folder.
These files are normally deleted as soon as they are no longer needed.

Important!!
the process can open so many files simultaneously
that the system NOFILE limit is reached.
This limit should be raised with `ulimit -n 128000` to avoid terminating
the program. The minimum number for open file handles is at least:
<maximum number of shapes for N> * 32

- CubeSwapSet is specialized std::unordered_set<> that stores the cube data in a file.
- CubeStorage acts as pseudo allocator for the cube data.
- CubePtr is the key type inserted in to CubeSwapSet.
  This only an 64-bit offset into the backing file and
  CubePtr is owned by CubeStorage that created it.
- CubePtr::get(const CubeStorage&) reads out the Cube from the storage.
  Hashy users are adapted to use it where needed.
- Clearing Hashy is now quite fast because there is no memory to be
  freed for CubePtrs. SubsubHashy::clear() simply deletes the data
  and the backing file.
- Compiling in C++20 mode enables speed up by allowing
  SubsubHashy::contains() to work with Cube and CubePtr types.

Signed-off-by: JATothrim <jarmo.tiitto@gmail.com>
  • Loading branch information
JATothrim committed Aug 21, 2023
1 parent b79f161 commit 64278c8
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 54 deletions.
1 change: 1 addition & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ add_library(CubeObjs OBJECT
"src/cubes.cpp"
"src/rotations.cpp"
"src/newCache.cpp"
"src/cubeSwapSet.cpp"
)
ConfigureTarget(CubeObjs)

Expand Down
178 changes: 178 additions & 0 deletions cpp/include/cubeSwapSet.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#pragma once
#ifndef OPENCUBES_CUBE_DISKSWAP_SET_HPP
#define OPENCUBES_CUBE_DISKSWAP_SET_HPP

#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <filesystem>

#include "cube.hpp"
#include "mapped_file.hpp"

/**
* Implement std::unordered_set<> that stores element data in a file.
*
* Cubes stored with size N in the set have constant cost of RAM memory:
* Only the std::unordered_set<> itself and the internals nodes are stored in RAM.
* The element *data* (i.e. XYZ data) is stored in the file.
* The performance cost is that each time the element is accessed
* the data has to be read back from the file.
* (Iterating the entire CubeSwapSet involves reading the entire backing file)
*
* Clearing the CubeSwapSet does not release the backing file space managed by CubeStorage.
* Call to CubeStorage::discard() is required after clearing or destructing
* the CubeSwapSet instance to cleanup the file.
* Elements cannot be removed one-by-one.
*/
class CubeStorage;

/**
* Overlay that reads the cube data from the backing file.
* CubePtr needs its associated CubeStorage instance to be able to
* access its contents with CubePtr::get()
* The associated CubeStorage owning the CubePtr
* should always be available where CubePtr is used.
*/
class CubePtr {
protected:
mapped::seekoff_t m_seek = 0;

public:
explicit CubePtr(mapped::seekoff_t offset) : m_seek(offset) {}
CubePtr(const CubePtr& c) : m_seek(c.m_seek) {}

/**
* Get the Cube pointed by this instance.
*/
Cube get(const CubeStorage& storage) const;

template <typename Itr>
void copyout(const CubeStorage& storage, size_t n, Itr out) const {
auto tmp = get(storage);
std::copy_n(tmp.begin(), n, out);
}

mapped::seekoff_t seek() const { return m_seek; }
};

/**
* Stateful comparator for Cubeptr
*/
class CubePtrEqual {
protected:
const CubeStorage* m_storage = nullptr;
public:
// C++20 feature:
using is_transparent = void;

CubePtrEqual(const CubeStorage* ctx) : m_storage(ctx) {}
CubePtrEqual(const CubePtrEqual& ctx) : m_storage(ctx.m_storage) {}

bool operator()(const CubePtr& a, const CubePtr& b) const { return a.get(*m_storage) == b.get(*m_storage); }

bool operator()(const Cube& a, const CubePtr& b) const { return a == b.get(*m_storage); }

bool operator()(const CubePtr& a, const Cube& b) const { return a.get(*m_storage) == b; }
};

class CubePtrHash {
protected:
const CubeStorage* m_storage = nullptr;
public:
// C++20 feature:
using is_transparent = void;
using transparent_key_equal = CubePtrEqual;

CubePtrHash(const CubeStorage* ctx) : m_storage(ctx) {}
CubePtrHash(const CubePtrHash& ctx) : m_storage(ctx.m_storage) {}

size_t operator()(const Cube& x) const {
std::size_t seed = x.size();
for (auto& p : x) {
auto x = HashXYZ()(p);
seed ^= x + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
return seed;
}

size_t operator()(const CubePtr& x) const {
auto cube = x.get(*m_storage);
std::size_t seed = cube.size();
for (auto& p : cube) {
auto x = HashXYZ()(p);
seed ^= x + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
return seed;
}
};

class CubeStorage {
protected:
std::mutex m_mtx;
std::filesystem::path m_fpath;
std::shared_ptr<mapped::file> m_file;
std::unique_ptr<mapped::region> m_map;

static std::atomic<int> m_init_num;
const size_t m_cube_size;
mapped::seekoff_t m_prev_seek = 0;
mapped::seekoff_t m_alloc_seek = 0;

public:
/**
* Initialize Cube file storage
* @param fname directory where to store the backing file.
* @param n The storage is reserved in n sized chunks.
* This should be equal to Cube::size() that are passed into allocate()
* as no other allocation size is supported.
* @note the backing file creation is delayed until allocate() is called first time.
*/
CubeStorage(std::filesystem::path path, size_t n);
~CubeStorage();

// not copyable
CubeStorage(const CubeStorage&) = delete;
CubeStorage& operator=(const CubeStorage&) = delete;
// move constructible: but only if no allocations exists
CubeStorage(CubeStorage&& mv);
CubeStorage& operator=(CubeStorage&& mv) = delete;

size_t cubeSize() const { return m_cube_size; }

/**
* Store Cube data into the backing file.
* Returns CubePtr that can be inserted into CubeSwapSet.
* @note cube.size() must be equal to this->cubeSize()
*/
CubePtr allocate(const Cube& cube);

/**
* Revert the effect of last allocate()
*/
void cancel_allocation();

/**
* Retrieve the cube data from the backing file.
*/
Cube read(const CubePtr& x) const;

/**
* Drop all stored data.
* Shrinks the backing file to zero size and deletes it.
*/
void discard();
};

/**
* CubeStorage enabled std::unordered_set<>
*
* The CubeSwapSet must be constructed with already initialized
* stateful instances of CubePtrEqual and CubePtrHash functors
* that resolve the CubePtr instance using the CubeStorage instance.
*/
using CubeSwapSet = std::unordered_set<CubePtr, CubePtrHash, CubePtrEqual>;

#endif
86 changes: 66 additions & 20 deletions cpp/include/hashes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
#define OPENCUBES_HASHES_HPP
#include <array>
#include <cstdio>
#include <deque>
#include <filesystem>
#include <map>
#include <shared_mutex>
#include <unordered_set>
#include <vector>

#include "cube.hpp"
#include "cubeSwapSet.hpp"
#include "utils.hpp"

struct HashCube {
Expand All @@ -27,24 +30,30 @@ using CubeSet = std::unordered_set<Cube, HashCube, std::equal_to<Cube>>;

class Subsubhashy {
protected:
CubeSet set;
CubeStorage set_storage;
CubeSwapSet set;
mutable std::shared_mutex set_mutex;

public:
explicit Subsubhashy(std::filesystem::path path, size_t n) : set_storage(path, n), set(1, CubePtrHash(&set_storage), CubePtrEqual(&set_storage)) {}

template <typename CubeT>
void insert(CubeT &&c) {
std::lock_guard lock(set_mutex);
set.emplace(std::forward<CubeT>(c));
auto [itr, isnew] = set.emplace(set_storage.allocate(std::forward<CubeT>(c)));
if (!isnew) {
set_storage.cancel_allocation();
}
}

#if __cplusplus > 201703L
// todo: need C++17 equivalent for *generic*
// contains() or find() that accepts both Cube and CubePtr types
bool contains(const Cube &c) const {
std::shared_lock lock(set_mutex);
auto itr = set.find(c);
if (itr != set.end()) {
return true;
}
return false;
return set.contains<Cube>(c);
}
#endif

auto size() const {
std::shared_lock lock(set_mutex);
Expand All @@ -57,27 +66,45 @@ class Subsubhashy {
set.reserve(1);
}

// Get CubeStorage instance.
// [this->begin(), this->end()] iterated CubePtr's
// Can be resolved with CubePtr::get(this->storage())
// that returns copy of the data as Cube.
const CubeStorage &storage() const { return set_storage; }

auto begin() const { return set.begin(); }
auto end() const { return set.end(); }
auto begin() { return set.begin(); }
auto end() { return set.end(); }
};

template <int NUM>
class Subhashy {
protected:
std::array<Subsubhashy, NUM> byhash;
std::deque<Subsubhashy> byhash;

public:
Subhashy(int NUM, size_t N, std::filesystem::path path) {
for (int i = 0; i < NUM; ++i) {
byhash.emplace_back(path, N);
}
}

template <typename CubeT>
void insert(CubeT &&c) {
HashCube hash;
auto idx = hash(c) % NUM;
auto idx = hash(c) % byhash.size();
auto &set = byhash[idx];
if (!set.contains(c)) set.insert(std::forward<CubeT>(c));
#if __cplusplus > 201703L
if (set.contains(c)) return;
#endif
set.insert(std::forward<CubeT>(c));
// printf("new size %ld\n\r", byshape[shape].size());
}

void clear() {
for (auto &set : byhash) set.clear();
}

auto size() const {
size_t sum = 0;
for (auto &set : byhash) {
Expand All @@ -95,7 +122,9 @@ class Subhashy {

class Hashy {
protected:
std::map<XYZ, Subhashy<32>> byshape;
std::map<XYZ, Subhashy> byshape;
std::filesystem::path base_path;
int N;
mutable std::shared_mutex set_mutex;

public:
Expand All @@ -111,24 +140,41 @@ class Hashy {
return out;
}

explicit Hashy(std::string path = ".") : base_path(path) {}

void init(int n) {
// create all subhashy which will be needed for N
std::lock_guard lock(set_mutex);
for (auto s : generateShapes(n)) byshape[s].size();
N = n;
for (auto s : generateShapes(n)) {
initSubHashy(n, s);
}
std::printf("%ld sets by shape for N=%d\n\r", byshape.size(), n);
}

Subhashy<32> &at(XYZ shape) {
Subhashy &initSubHashy(int n, XYZ s) {
assert(N == n);

auto itr = byshape.find(s);
if (itr == byshape.end()) {
auto [itr, isnew] = byshape.emplace(s, Subhashy(32, n, base_path));
assert(isnew);
itr->second.size();
return itr->second;
} else {
return itr->second;
}
}

Subhashy &at(XYZ shape) {
std::shared_lock lock(set_mutex);
auto itr = byshape.find(shape);
if (itr != byshape.end()) {
return itr->second;
}
lock.unlock();
// Not sure if this is supposed to happen normally
// if init() creates all subhashys required.
std::lock_guard elock(set_mutex);
return byshape[shape];
// should never get here...
std::printf("BUG: missing shape [%2d %2d %2d]:\n\r", shape.x(), shape.y(), shape.z());
std::abort();
return *((Subhashy *)0);
}

template <typename CubeT>
Expand Down
5 changes: 2 additions & 3 deletions cpp/include/newCache.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,8 @@ class FlatCache : public ICache {
for (auto& [shape, set] : hashes) {
auto begin = allXYZs.data() + allXYZs.size();
for (auto& subset : set) {
for (auto& cube : subset)
// allXYZs.emplace_back(allXYZs.end(), subset.set.begin(), subset.set.end());
std::copy(cube.begin(), cube.end(), std::back_inserter(allXYZs));
for (auto& cubeptr : subset)
cubeptr.copyout(subset.storage(), n, std::back_inserter(allXYZs));
}
auto end = allXYZs.data() + allXYZs.size();
// std::printf(" SR %p %p\n", (void*)begin, (void*)end);
Expand Down
Loading

0 comments on commit 64278c8

Please sign in to comment.